当前位置: 移动技术网 > IT编程>开发语言>Java > SpringMVC实现文件上传和下载的工具类

SpringMVC实现文件上传和下载的工具类

2020年06月23日  | 移动技术网IT编程  | 我要评论

本文主要目的是记录自己基于springmvc实现的文件上传和下载的工具类的编写,代码经过测试可以直接运行在以后的项目中。

开发的主要思路是对上传和下载文件进行抽象,把上传和下载的核心功能抽取出来分装成类。

我的工具类具体代码如下:

package com.baosight.utils;
 
import java.io.bufferedinputstream;
import java.io.file;
import java.io.filenotfoundexception;
import java.io.fileoutputstream;
import java.io.ioexception;
import java.io.inputstream;
import java.util.arraylist;
import java.util.arrays;
import java.util.iterator;
import java.util.list;
import java.util.properties;
 
 
import org.apache.commons.fileupload.util.streams;
import org.apache.commons.io.fileutils;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.http.httpheaders;
import org.springframework.http.httpstatus;
import org.springframework.http.mediatype;
import org.springframework.http.responseentity;
import org.springframework.web.multipart.multipartfile;
 
/**
 * springmvc中文件的工具类
 * 
 * @author chenpeng
 *
 */
public class myfileutils {
 
 private static final logger logger = loggerfactory.getlogger(myfileutils.class);
 /**
 * 存储的路径,默认的为c盘符
 */
 public static string directory = "c:/";
 
 /**
 * 图片的大小配置,默认为2048000
 */
 public static long imagesize = 2048000;
 
 /**
 * 图片的类型配置默认如下
 */
 public static string imagetype = "image/bmp,image/png,image/jpeg,image/jpg";
 
 /**
 * 文件上传的大小限制
 */
 public static long filesize = 20971520;
 
 /**
 * 文件上传文件的类型
 */
 public static string filetype = "application/msword,application/pdf,application/zip,video/mpeg,video/quicktime,video/x-sgi-movie,video/mpeg,video/x-msvideo,audio/x-mpeg,application/octet-stream";
 
 /**
 * 配置路径的字段
 */
 public static final string directory = "myfileutils.uploadfiledirectory";
 
 /**
 * 配置图片大小的字段
 */
 public static final string imagesize = "myfileutils.imgsize";
 
 /**
 * 配置图片类型的字段
 */
 public static final string imagetype = "myfileutils.imgtype";
 
 /**
 * 文件的大小
 */
 public static final string filesize = "myfileutils.filesize";
 
 /**
 * 文件的类型
 */
 public static final string filetype = "myfileutils.filetype";
 
 /**
 * 文件所支持的全部类型(图片,office文件,压缩包,视频,音频)
 */
 public static string[] sporting_filetype;
 
 /**
 * 上传的结果
 * 
 * @author cp upload_success:"上传成功" upload_size_error:"上传文件过大"
 *   upload_type_error:"上传文件类型错误" upload_file:"上传文件失败"
 *
 */
 public static enum upload {
 upload_success("上传成功", 1), upload_size_error("上传文件过大", 2), upload_type_error("上传文件类型错误",
 3), upload_file("上传文件失败", 4),file_download_success("文件下载成功",5),file_notfound("未找到该文件",6);
 
 // 成员变量
 private string name;
 @suppresswarnings("unused")
 private int index;
 
 // 构造方法
 private upload(string name, int index) {
 this.name = name;
 this.index = index;
 }
 
 // 覆盖tostring方法
 @override
 public string tostring() {
 return this.name;
 }
 
 }
 
 /**
 * static语句块读取配置文件
 */
 static {
 properties properties = new properties();
 try {
 inputstream in = new bufferedinputstream(myfileutils.class.getresourceasstream("/user-setting.properties"));
 properties.load(in);
 iterator<string> it = properties.stringpropertynames().iterator();
 while (it.hasnext()) {
 string key = it.next();
 string value = properties.getproperty(key);
 logger.info("读取配置文件..." + key + ":" + value);
 switch (key) {
 // 如果是路径
 case directory:
  // 判断是否存在该文件夹如果没有则要重新创建
  file file = new file(value);
  if (!file.exists() && !file.isdirectory()) {
  // 创建文件夹
  file.mkdirs();
  }
  directory = value;
  break;
 // 如果是图片的格式
 case imagetype:
  imagetype = value;
  break;
 // 如果是图片的大小
 case imagesize:
  imagesize = long.parselong(value);
  break;
 // 如果是文件的大小
 case filesize:
  filesize = long.parselong(value);
  break;
 // 如果是文件的类型
 case filetype:
  filetype = value;
  break;
 default:
  break;
 }
 }
 // 读取配置后把支持图片的和支持文件的放在一起
 string[] imagetypes = imagetype.split(",");
 string[] filetypees = filetype.split(",");
 sporting_filetype = new string[imagetypes.length+filetypees.length];
 system.arraycopy(imagetypes, 0, sporting_filetype, 0, imagetypes.length);
 system.arraycopy(filetypees, 0, sporting_filetype, imagetypes.length, filetypees.length);
 } catch (filenotfoundexception e) {
 logger.error("未发现配置文件");
 } catch (ioexception e) {
 logger.error("读取配置文件失败");
 }
 }
 
 /**
 * 上传文件
 * 
 * @param multipartfiles
 *   上传的文件
 * @param parentdir
 *   上传文件制定的父文件夹,格式为a/b,可以为空
 * @param storagefilename
 *   上传的文件的别名,注意上传文件名不用带后缀
 * @return
 */
 public static list<string> uploadfile(multipartfile[] multipartfiles, string parentdir, string storagefilename) {
 list<string> mystrings = new arraylist<>();
 if (multipartfiles != null && multipartfiles.length > 0) {
 string lastring = "";
 string storagepath = "";
 // 先判断要存储的文件夹是否存在,如果不存在就重新创建
 string path = directory + ((parentdir!=null)?("/" + parentdir):"");
 file filedir = new file(path);
 if (!filedir.exists() && !filedir.isdirectory()) {
 filedir.mkdirs();
 }
 for (multipartfile file : multipartfiles) {
 // 先判断文件的类型是否符合配置要求
 if (myfileutils.checkfiletype(file, sporting_filetype)) {
  /***** 判断文件的大小 *****/
  string type = file.getcontenttype().substring(0, file.getcontenttype().lastindexof("/"));
  // 如果是图片,且大小超过范围的,如果是文件,且大小超过范围的直接返回
  if ((type.equals("image") && !myfileutils.checkfilesize(file, myfileutils.imagesize))
  || (!type.equals("image") && !myfileutils.checkfilesize(file, filesize))) {
  logger.info("文件过大");
  
  }
  // 获取变量名file,文件类型,文件名
  logger.info(
  "上传的文件:" + file.getname() + "," + file.getcontenttype() + "," + file.getoriginalfilename());
  lastring = file.getoriginalfilename().substring(file.getoriginalfilename().lastindexof("."));
  try {
  if (!file.isempty()) {
  // 判断sotragename是否为空,如果不为空就以存储的命名,为空就以原来的名称命名
  storagepath = path + "/" + ((storagefilename != null) ? (storagefilename + lastring)
   : file.getoriginalfilename());
  logger.info("保存的路径为:" + storagepath);
  streams.copy(file.getinputstream(), new fileoutputstream(storagepath), true);    
  mystrings.add(storagepath);
  }
  } catch (exception e) {
  logger.error("上传文件失败");
  e.printstacktrace();  
  }
 } else {
  logger.info("上传文件类型有误");  
 }
 }
 logger.info("上传文件成功"); 
 } else {
 logger.info("上传文件失 败"); 
 }
 return mystrings;
 }
 
 /** 
 * 下载文件
 * 
 * @param filepath
 *   文件的路径
 * @param filerename
 *   文件的下载显示的别名
 * @return 文件的对象
 * @throws ioexception
 */
 public static responseentity<byte[]> downloadfile(string filepath, string filerename) throws ioexception {
 // 指定文件,必须是绝对路径
 file file = new file(filepath);
 // 下载浏览器响应的那个文件名
 string dfilename = new string(filerename.getbytes("gbk"), "iso-8859-1");
 // 下面开始设置httpheaders,使得浏览器响应下载
 httpheaders headers = new httpheaders();
 // 设置响应方式
 headers.setcontenttype(mediatype.application_octet_stream);
 // 设置响应文件
 headers.setcontentdispositionformdata("attachment", dfilename);
 // 把文件以二进制形式写回
 responseentity<byte[]> result = null;
 try { 
 result = new responseentity<byte[]>(fileutils.readfiletobytearray(file), headers, httpstatus.created);
 } catch (exception e) {
 logger.error(e.tostring());
 }
 return result;
 }
 
 /**
 * 检查文件的格式
 * 
 * @return
 */
 public static boolean checkfiletype(multipartfile file, string[] supprtedtypes) {
 string filetype = file.getcontenttype();
 logger.info("文件的格式为:"+filetype);
 return arrays.aslist(supprtedtypes).contains(filetype);
 }
 
 /**
 * 判断文件的大小是否符合要求
 * 
 * @param maxsize
 *   最大文件
 * @return
 */
 public static boolean checkfilesize(multipartfile file, long maxsize) {
 logger.info("文件的大小比较:"+file.getsize()+",max:"+maxsize);
 return file.getsize() <= maxsize;
 }
 
 
}

需要注意的是我的工具类使用了如下的配置,对自己的业务需求进行可配置,提高代码的应用场景(user-setting.properties):

# 上传文件的父目录
# 示例:myfileutils.uploadfiledirectory= e:/emp
myfileutils.uploadfiledirectory= d:/myfiles
# 上传图片的大小
# 示例: myfileutils.imgsize= 2097152
myfileutils.imgsize= 2097152
# 上传图片的格式
# 示例:myfileutils.imgtype= image/bmp,image/png,image/jpeg,image/jpg,image/x-png,image/pjpeg
myfileutils.imgtype= image/bmp,image/png,image/jpeg,image/jpg,image/x-png,image/pjpeg
# 上传文件的大小(除了图片)
# 示例:myfileutils.filesize= 20971520
myfileutils.filesize= 20971520
# 上传文件的类型(除了图片)
# 示例:myfileutils.filetype= application/msword,application/pdf,application/zip,video/mpeg,video/quicktime,video/x-sgi-movie,video/mpeg,video/x-msvideo,audio/x-mpeg,application/octet-stream
myfileutils.filetype= video/avi,application/vnd.ms-powerpoint,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/msword,application/pdf,application/zip,video/mpeg,video/quicktime,video/x-sgi-movie,video/mpeg,video/x-msvideo,audio/x-mpeg,application/octet-stream

另外本工具类中为了方便调试错误与log4j进行了集成,所以还需要如下的配置文件:

#定义log输出级别 
log4j.rootlogger=info,console,file 
#定义日志输出目的地为控制台 
log4j.appender.console=org.apache.log4j.consoleappender 
log4j.appender.console.target=system.out 
#可以灵活地指定日志输出格式,下面一行是指定具体的格式 
log4j.appender.console.layout = org.apache.log4j.patternlayout 
log4j.appender.console.layout.conversionpattern=[%c] - %m%n 
 
#文件大小到达指定尺寸的时候产生一个新的文件 
log4j.appender.file = org.apache.log4j.rollingfileappender 
#指定输出目录 
log4j.appender.file.file = logs/ssm.log 
#定义文件最大大小 
log4j.appender.file.maxfilesize = 10mb 
# 输出所以日志,如果换成debug表示输出debug以上级别日志 
log4j.appender.file.threshold = all 
log4j.appender.file.layout = org.apache.log4j.patternlayout 
log4j.appender.file.layout.conversionpattern =[%p] [%d{yyyy-mm-dd hh\:mm\:ss}][%c]%m%n 
 
log4j.logger.com.ibatis=debug 
log4j.logger.com.ibatis.common.jdbc.simpledatasource=debug 
log4j.logger.com.ibatis.common.jdbc.scriptrunner=debug 
log4j.logger.com.ibatis.sqlmap.engine.impl.sqlmapclientdelegate=debug 
log4j.logger.java.sql.connection=debug 
log4j.logger.java.sql.statement=debug 
log4j.logger.java.sql.preparedstatement=debug

下面就开始来重新创建一个maven的web项目来测试使用下我的工具类:

1.我使用的开发工具为idea,新建一个maven web的项目,然后打开pom.xml配置文件。首先来搭建一下springmvc的开发环境,需要引用相关的jar包,maven的配置文件如下:

<?xml version="1.0" encoding="utf-8"?>
 
<project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
 xsi:schemalocation="http://maven.apache.org/pom/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelversion>4.0.0</modelversion>
 
 <groupid>com.baosight</groupid>
 <artifactid>testfile</artifactid>
 <version>1.0-snapshot</version>
 <packaging>war</packaging>
 
 <name>testfile maven webapp</name>
 <url>http://www.example.com</url>
 
 <properties>
 <project.build.sourceencoding>utf-8</project.build.sourceencoding>
 <maven.compiler.source>1.7</maven.compiler.source>
 <maven.compiler.target>1.7</maven.compiler.target>
  <!-- spring版本号 -->
  <spring.version>4.0.5.release</spring.version>
  <!-- log4j日志文件管理包版本 -->
  <slf4j.version>1.7.7</slf4j.version>
  <log4j.version>1.2.17</log4j.version>
 </properties>
 
 <dependencies>
 
 <dependency>
  <groupid>junit</groupid>
  <artifactid>junit</artifactid>
  <version>4.11</version>
  <scope>test</scope>
 </dependency>
 
  <!-- spring核心包 -->
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-core</artifactid>
   <version>${spring.version}</version>
  </dependency>
 
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-web</artifactid>
   <version>${spring.version}</version>
  </dependency>
 
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-oxm</artifactid>
   <version>${spring.version}</version>
  </dependency>
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-tx</artifactid>
   <version>${spring.version}</version>
  </dependency>
 
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-jdbc</artifactid>
   <version>${spring.version}</version>
  </dependency>
 
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-webmvc</artifactid>
   <version>${spring.version}</version>
  </dependency>
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-aop</artifactid>
   <version>${spring.version}</version>
  </dependency>
 
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-context-support</artifactid>
   <version>${spring.version}</version>
  </dependency>
 
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-test</artifactid>
   <version>${spring.version}</version>
  </dependency>
 
  <dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-aspects</artifactid>
   <version>${spring.version}</version>
  </dependency>
 
  <!-- 上传组件包 -->
  <dependency>
   <groupid>commons-fileupload</groupid>
   <artifactid>commons-fileupload</artifactid>
   <version>1.3.1</version>
  </dependency>
  <dependency>
   <groupid>commons-io</groupid>
   <artifactid>commons-io</artifactid>
   <version>2.2</version>
  </dependency>
  <dependency>
   <groupid>commons-codec</groupid>
   <artifactid>commons-codec</artifactid>
   <version>1.9</version>
  </dependency>
 
  <!--日志包-->
  <dependency>
   <groupid>org.slf4j</groupid>
   <artifactid>slf4j-api</artifactid>
   <version>${slf4j.version}</version>
  </dependency>
 
  <dependency>
   <groupid>org.slf4j</groupid>
   <artifactid>slf4j-log4j12</artifactid>
   <version>${slf4j.version}</version>
  </dependency>
 
  <dependency>
   <groupid>javax.servlet</groupid>
   <artifactid>jstl</artifactid>
   <version>1.2</version>
  </dependency>
 
  <!-- 导入java ee jar 包 -->
  <dependency>
   <groupid>javax</groupid>
   <artifactid>javaee-api</artifactid>
   <scope>provided</scope>
   <version>7.0</version>
  </dependency>
 
 </dependencies>
 
 <build>
 <finalname>testfile</finalname>
 <pluginmanagement><!-- lock down plugins versions to avoid using maven defaults (may be moved to parent pom) -->
  <plugins>
  <plugin>
   <artifactid>maven-clean-plugin</artifactid>
   <version>3.0.0</version>
  </plugin>
  <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#plugin_bindings_for_war_packaging -->
  <plugin>
   <artifactid>maven-resources-plugin</artifactid>
   <version>3.0.2</version>
  </plugin>
  <plugin>
   <artifactid>maven-compiler-plugin</artifactid>
   <version>3.7.0</version>
  </plugin>
  <plugin>
   <artifactid>maven-surefire-plugin</artifactid>
   <version>2.20.1</version>
  </plugin>
  <plugin>
   <artifactid>maven-war-plugin</artifactid>
   <version>3.2.0</version>
  </plugin>
  <plugin>
   <artifactid>maven-install-plugin</artifactid>
   <version>2.5.2</version>
  </plugin>
  <plugin>
   <artifactid>maven-deploy-plugin</artifactid>
   <version>2.8.2</version>
  </plugin>
  </plugins>
 </pluginmanagement>
 </build>
</project>

配置好pom,然后就来对springmvc进行相关的配置,首先按照如下的格式创建好对应的文件,里面的配置一一来说明:

1.首先是log4j.properties见上面的配置即可,拷贝到里面并保存;

2.然后是springmvc的配置:

<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xmlns:p="http://www.springframework.org/schema/p"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:mvc="http://www.springframework.org/schema/mvc"
 xsi:schemalocation="http://www.springframework.org/schema/beans 
 http://www.springframework.org/schema/beans/spring-beans-3.1.xsd 
 http://www.springframework.org/schema/context 
 http://www.springframework.org/schema/context/spring-context-3.1.xsd 
 http://www.springframework.org/schema/mvc 
 http://www.springframework.org/schema/mvc/spring-mvc.xsd">
 
 
 <!-- 自动扫描该包,使springmvc认为包下用了@controller注解的类是控制器 -->
 <context:component-scan base-package="com.baosight.controller" />
 
 <mvc:annotation-driven/>
 
 <!-- 定义跳转的文件的前后缀 ,视图模式配置 -->
 <bean id="viewresolver"
 class="org.springframework.web.servlet.view.internalresourceviewresolver">
 <!-- 这里的配置我的理解是自动给后面action的方法return的字符串加上前缀和后缀,变成一个 可用的url地址 -->
 <property name="prefix" value="/web-inf/views/" />
 <property name="suffix" value=".jsp" />
 </bean>
 
 <!-- 配置文件上传,如果没有使用文件上传可以不用配置,当然如果不配,那么配置文件中也不必引入上传组件包 -->
 <bean id="multipartresolver"
 class="org.springframework.web.multipart.commons.commonsmultipartresolver">
 <!-- 默认编码 -->
 <property name="defaultencoding" value="utf-8" />
 <!-- 文件大小最大值 -->
 <property name="maxuploadsize" value="10485760000" />
 <!-- 内存中的最大值 -->
 <property name="maxinmemorysize" value="40960" />
 </bean>
 
</beans>

具体配置的内容包括启用注解功能,定义springmvc的视图解析器,定义上传文件的大小和编码;

3.其次是user-setting.properties 里面具体内容见上面的用户配置,主要限定了上传文件的类型和大小的问题;

4.最后是对web.xml进行配置:

<!doctype web-app public
 "-//sun microsystems, inc.//dtd web application 2.3//en"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
 
<web-app>
 <display-name>archetype created web application</display-name>
 
 <!-- spring mvc servlet -->
 <servlet>
 <servlet-name>springmvc</servlet-name>
 <servlet-class>org.springframework.web.servlet.dispatcherservlet</servlet-class>
 <init-param>
  <param-name>contextconfiglocation</param-name>
  <param-value>classpath:spring-mvc.xml</param-value>
 </init-param>
 <load-on-startup>1</load-on-startup>
 </servlet>
 <!-- 配置spring mvc servlet的映射地址 -->
 <servlet-mapping>
 <servlet-name>springmvc</servlet-name>
 <url-pattern>/</url-pattern>
 </servlet-mapping>
 
</web-app>

具体包括定义spring的分发器,指定springmvc文件配置的位置,以及servlet映射的地址;

这样我们的springmvc的开发环境算是搭建完成了。

2.下面将我们的工具类拷贝到项目中的自己指定的位置:

3.下面开始正式对我们的工具类进行上传文件和下载文件的测试了:

1).首先编写index.jsp页面:

<%--
 created by intellij idea.
 user: chenpeng
 date: 2018/7/21
 time: 17:03
 to change this template use file | settings | file templates.
--%>
<%@ page contenttype="text/html;charset=utf-8" language="java" %>
<html>
<head>
 <title>title</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
 <p>选择文件:<input type="file" name="files"></p>
 <p><input type="submit" value="提交"></p>
</form>
</body>
</html>

这样页面的效果为:

接着我们来编写springmvc的后台:

package com.baosight.controller;
 
import com.baosight.utils.myfileutils;
import org.springframework.context.annotation.scope;
import org.springframework.http.responseentity;
import org.springframework.stereotype.controller;
import org.springframework.ui.modelmap;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.requestmethod;
import org.springframework.web.bind.annotation.requestparam;
import org.springframework.web.bind.annotation.responsebody;
import org.springframework.web.multipart.multipartfile;
import org.springframework.web.servlet.modelandview;
 
import javax.servlet.http.httpservletrequest;
import java.io.ioexception;
import java.util.list;
 
@controller
@scope("prototype")
@requestmapping("/")
public class uploadfiletest {
 
 /**
  * 上传文件测试
  * @param files
  * @param map
  * @return
  */
 @requestmapping(value = "/upload",method = requestmethod.post)
 public string upload(@requestparam("files") multipartfile[] files, modelmap map){
  list<string> results = myfileutils.uploadfile(files,"ds/sd","测试上传的文件");
  if(results!=null && results.size()>0){
   map.addattribute("urls", results.get(0));
  }
  return "success";
 }
 
 /**
  * 下载文件测试
  * @param url
  * @return
  * @throws ioexception
  */
 @requestmapping(value = "/download")
 public responseentity<byte[]> getfile(@requestparam("url")string url) throws ioexception {
  return myfileutils.downloadfile(url,"下载的文件"+ url.substring(url.lastindexof(".")));
 }
}

下载成功的页面的jsp为:

<%@ page contenttype="text/html;charset=utf-8" language="java" iselignored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
 <title>title</title>
</head>
<body>
 <h1>success!</h1>
 <br>
 <a href="/download?url=${urls}" rel="external nofollow" >下载上传的文件</a>
</body>
</html>

里面提供了下载的按钮(根据我们springmvc的视图解析器的配置我们的所有页面都在web-inf/views/文件夹下)。

特别注意的是页面上要启用el表达式否则el表达式在页面上会失效!到现在为止,我们的代码工作已经编写完成了,接着进行测试:

这样我们文件上传和下载的任务就算完成了。

备注:

1.用户可以再user-setting.properties中指定文件存放的更目录

2.用户在上传文件还可以指定次级目录,比如根目录+(次级目录)+(新指定的文件名).文件后缀

3.文件都是保存在服务器内部的,能够保存小量的文件对于大型的文件需要考虑其他专门的文件存储的解决方案,本工具类能够实现中小型项目文件上传和下载的任务。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网