详解HTTPWeb及网络基础为了更好的理解什么是HTTP协议，我们需要先了解TCP/IP协议族。目前通常使用的网络都是在基于TCP/IP协议族的情况下进行信息传输，而HTTP也实际上是属于TCP/IP协议模型。要知道，不同的计算机之间要通过网络进行通信，那么必须遵循统一的规定，才能进行交流。这也就是协议的作用，通过制定规约，让不同的计算机可以进行信息互换。TCP/IP协议最重要的一点是对于信息加工传输进行了分层处理，这种分层结构清晰明了。TCP/IP协议族按层次划分为应用层、网络层、传输层、数据链路层。发送端数据依次经过应用层、网络层、传输层、数据链路层；接收端依次从数据链路层、传输层、网络层、应用层接收数据。而我们的HTTP协议就应用在应用层。HTTP报文用于HTTP协议交互的信息被称为是HTTP报文。客户端的HTTP报文叫做请求报文，响应端的HTTP报文称为响应报文。HTTP的报文是由多行数据构成的字符串文本。其结构大致可以分为报文首部和报文主体两块，两者通过空行进行划分，通常，报文是没有主体的。HTTP结构HTTP请求报文分为报文首部、空行、报文主体，其中报文首部内存放了请求行、请求首部字段、通用首部字段、实体首部字段和其他。HTTP响应报文分为报文首部、空行、报文主体，其中报文首部分为状态行、响应首部字段、通用首部字段、实体首部字段以及其他。HTTP状态码1XX：通知1XX系列响应代码仅在与HTTP服务器沟通时使用。100("Continue") 重要程度：中等，但（写操作时）很少用。这是对HTTP LBYL（look-before-you-leap）请求的一个可能的响应。该响应代码表明：客户端应重新发送初始请求，并在请求中附上第一次请求时未提供的（可能很大或者包含敏感信息的）表示。客户端这次发送的请求不会被拒绝。对LBYL请求的另一个可能的响应是417("Expectation Failed")。请求报头：要做一个LBYL请求，客户端必须把Expect请求报头设为字符串"100-continue"。除此以外，客户端还需要设置其他一些报头，服务器将根据这些报头决定是响应100还是417。101("Switching Protocols") 重要程度：非常低。当客户端通过在请求里使用Upgrade报头，以通知服务器它想改用除HTTP协议之外的其他协议时，客户端将获得此响应代码。101响应代码表示“行，我现在改用另一个协议了”。通常HTTP客户端会在收到服务器发来的101响应后关闭与服务器的TCP连接。101响应代码意味着，该客户端不再是一个HTTP客户端，而将成为另一种客户端。尽管可以通过Upgrade报头从HTTP切换到HTTPS，或者从HTTP1.1切换到某个未来的版本，但实际使用Upgrade报头的情况比较少。Upgrade报头也可用于HTTP切换到一个完全不同的协议（如IRC）上，但那需要在Web服务器切换为一个IRC服务器的同时，Web客户端切换为一个IRC的客户端，因为服务器将立刻在同一个TCP连接上开始使用新的协议。请求报头：客户端把Upgrade报头设置为一组希望使用的协议。响应报头：如果服务器同意切换协议，它就返回一个Upgrade报头，说明它将切换到那个协议，并附上一个空白行。服务器不用关闭TCP链接，而是直接在该TCP连接上开始使用新的协议。2XX: 成功2XX系列响应代码表明操作成功了。200("OK") 重要程度：非常高。一般来说，这是客户端希望看到的响应代码。它表示服务器成功执行了客户端所请求的动作，并且在2XX系列里没有其他更适合的响应代码了。实体主体：对于GET请求，服务器应返回客户端所请求资源的一个表示。对于其他请求，服务器应返回当前所选资源的一个表示，或者刚刚执行的动作的一个描述。-201("Created")重要程度：高。当服务器依照客户端的请求创建了一个新资源时，发送此响应代码。响应报头：Location报头应包含指向新创建资源的规范URI。实体主体：应该给出新创建资源的描述与链接。若已经在Location报头里给出了新资源的URI，那么可以用新资源的一个表示作为实体主体。-202("Accepted")重要程度：中等。客户端的请求无法或将不被实时处理。请求稍后会被处理。请求看上去是合法的，但在实际处理它时有出现问题的可能。若一个请求触发了一个异步操作，或者一个需要现实世界参与的动作，或者一个需要很长时间才能完成且没必要让Web客户端一直等待的动作时，这个相应代码是一个合适的选择。响应报头：应该把未处理完的请求暴露为一个资源，以便客户端稍后查询其状态。Location报头可以包含指向该资源的URI。实体主体：若无法让客户端稍后查询请求的状态，那么至少应该提供一个关于何时能处理该请求的估计。203("Non-Authoritative Information") 重要程度：非常低。这个响应代码跟200一样，只不过服务器想让客户端知道，有些响应报头并非来自该服务器--他们可能是从客户端先前发送的一个请求里复制的，或者从第三方得到的。响应报头：客户端应明白某些报头可能是不准确的，某些响应报头可能不是服务器自己生成的，所以服务器也不知道其含义。204("No Content") 重要程度：高。若服务器拒绝对PUT、POST或者DELETE请求返回任何状态信息或表示，那么通常采用此响应代码。服务器也可以对GET请求返回此响应代码，这表明“客户端请求的资源存在，但其表示是空的”。注意与304("Not Modified")的区别。204常常用在Ajax应用里。服务器通过这个响应代码告诉客户端：客户端的输入已被接受，但客户端不应该改变任何UI元素。实体主体：不允许。205("Reset Content") 重要程度：低。它与204类似，但与204不同的是，它表明客户端应重置数据源的视图或数据结构。假如你在浏览器里提交一个HTML表单，并得到响应代码204，那么表单里的各个字段值不变，可以继续修改它们；但假如得到的响应代码205，那么表单里的各个字段将被重置为它们的初始值。从数据录入方面讲：204适合对单条记录做一系列编辑，而205适于连续输入一组记录。206("Partial Content") 重要程度：对于支持部分GET（partial GET）的服务而言“非常高”，其他情况下“低”。它跟200类似，但它用于对部分GET请求（即使用Range请求报头的GET请求）的响应。部分GET请求常用于大型二进制文件的断点续传。请求报头：客户端为Range请求报头设置一个值。响应报头：需要提供Date报头。ETag报头与Content-Location报头的值应该跟正常GET请求相同。若实体主体是单个字节范围（byte range），那么HTTP响应里必须包含一个Content-Range报头，以说明本响应返回的是表示的哪个部分，若实体主体是一个多部分实体（multipart entity）（即该实体主体由多个字节范围构成），那么每一个部分都要有自己的Content-Range报头。实体主体：不是整个表示，而是一个或者多个字节范围。3XX 重定向3XX系列响应代码表明：客户端需要做些额外工作才能得到所需要的资源。它们通常用于GET请求。他们通常告诉客户端需要向另一个URI发送GET请求，才能得到所需的表示。那个URI就包含在Location响应报头里。300("Multiple Choices") 重要程度：低。若被请求的资源在服务器端存在多个表示，而服务器不知道客户端想要的是哪一个表示时，发送这个响应代码。或者当客户端没有使用Accept-*报头来指定一个表示，或者客户端所请求的表示不存在时，也发送这个响应代码。在这种情况下，一种选择是，服务器返回一个首选表示，并把响应代码设置为200，不过它也可以返回一个包含该资源各个表示的URI列表，并把响应代码设为300。响应报头：如果服务器有首选表示，那么它可以在Location响应报头中给出这个首选表示的URI。跟其他3XX响应代码一样，客户端可以自动跟随Location中的URI。实体主体：一个包含该资源各个表示的URI的列表。可以在表示中提供一些信息，以便用户作出选择。301("Moved Permanently") 重要程度：中等。服务器知道客户端试图访问的是哪个资源，但它不喜欢客户端用当前URI来请求该资源。它希望客户端记住另一个URI，并在今后的请求中使用那个新的URI。你可以通过这个响应代码来防止由于URI变更而导致老URI失效。响应报头：服务器应当把规范URI放在Location响应报头里。实体主体：服务器可以发送一个包含新URI的信息，不过这不是必需的。302("Found") 重要程度：应该了解，特别市编写客户端时。但我不推荐使用它。这个响应代码市造成大多数重定向方面的混乱的最根本原因。它应该是像307那样被处理。实际上，在HTTP 1.0中，响应代码302的名称是”Moved Temporarily”，不幸的是，在实际生活中，绝大多数客户端拿它像303一样处理。它的不同之处在于当服务器为客户端的PUT，POST或者DELETE请求返回302响应代码时，客户端要怎么做。为了消除这一混淆，在HTTP 1.1中，该响应代码被重命名为"Found"，并新加了一个响应代码307。这个响应代码目前仍在广泛使用，但它的含义市混淆的，所以我建议你的服务发送307或者303，而不要发送302.除非你知道正在与一个不能理解303或307的HTTP 1.0客户端交互。响应报头：把客户端应重新请求的那个URI放在Location报头里。实体主体：一个包含指向新URI的链接的超文本文档（就像301一样）。303("See Other") 重要程度：高。请求已经被处理，但服务器不是直接返回一个响应文档，而是返回一个响应文档的URI。该响应文档可能是一个静态的状态信息，也可能是一个更有趣的资源。对于后一种情况，303是一种令服务器可以“发送一个资源的表示，而不强迫客户端下载其所有数据”的方式。客户端可以向Location报头里的URI发送GET请求，但它不是必须这么做。303响应代码是一种规范化资源URI的好办法。一个资源可以有多个URIs，但每个资源的规范URI只有一个，该资源的所有其他URIs都通过303指向该资源的规范URI，例如：303可以把一个对http://www.example.com/software/current.tar.gz的请求重定向到http://www.example.com/software/1.0.2.tar.gz。响应报头：Location报头里包含资源的URI。实体主体：一个包含指向新URI的链接的超文本文档。304("Not Modified") 重要程度：高。这个响应代码跟204("No Content")类似：响应实体主体都必须为空。但204用于没有主体数据的情况，而304用于有主体数据，但客户端已拥有该数据，没必要重复发送的情况。这个响应代码可用于条件HTTP请求（conditional HTTP request).如果客户端在发送GET请求时附上了一个值为Sunday的If-Modified-Since报头，而客户端所请求的表示在服务器端自星期日（Sunday）以来一直没有改变过，那么服务器可以返回一个304响应。服务器也可以返回一个200响应，但由于客户端已拥有该表示，因此重复发送该表示只会白白浪费宽带。响应报头：需要提供Date报头。Etag与Content-Location报头的值，应该跟返回200响应时的一样。若Expires, Cache-Control及Vary报头的值自上次发送以来已经改变，那么就要提供这些报头。实体主体：不允许。305("Use Proxy") 重要程度：低。这个响应代码用于告诉客户端它需要再发一次请求，但这次要通过一个HTTP代理发送，而不是直接发送给服务器。这个响应代码使用的不多，因为服务器很少在意客户端是否使用某一特定代理。这个代码主要用于基于代理的镜像站点。现在，镜像站点（如http://www.example.com.mysite.com/）包含跟原始站点（如 http://www.example.com/）一样的内容，但具有不同的URI，原始站点可以通过307把客户端重新定向到镜像站点上。假如有基于代理的镜像站点，那么你可以通过把 http://proxy.mysite.com/设为代理，使用跟原始URI（http://www.example.com/）一样的URI来访问镜像站点。这里，原始站点example.com可以通过305把客户端路由到一个地理上接近客户端的镜像代理。web浏览器一般不能正确处理这个响应代码，这是导致305响应代码用的不多的另一个原因。响应报头：Location报头里包含代理的URI。306 未使用 重要程度：无306 响应代码没有在HTTP标准中定义过。307("Temporary Redirect") 重要程度：高。请求还没有被处理，因为所请求的资源不在本地：它在另一个URI处。客户端应该向那个URI重新发送请求。就GET请求来说，它只是请求得到一个表示，该响应代码跟303没有区别。当服务器希望把客户端重新定向到一个镜像站点时，可以用307来响应GET请求。但对于POST，PUT及DELETE请求，它们希望服务器执行一些操作，307和303有显著区别。对POST，PUT或者DELETE请求响应303表明：操作已经成功执行，但响应实体将不随本响应一起返回，若客户端想要获取响应实体主体，它需要向另一个URI发送GET请求。而307表明：服务器尚未执行操作，客户端需要向Location报头里的那个URI重新提交整个请求。响应报头： 把客户端应重新请求的那个URI放在Location报头里。实体主体：一个包含指向新URI的链接的超文本文档。4XX：客户端错误这些响应代码表明客户端出现错误。不是认证信息有问题，就是表示格式或HTTP库本身有问题。客户端需要自行改正。400("Bad Request") 重要程度：高。这是一个通用的客户端错误状态，当其他4XX响应代码不适用时，就采用400。此响应代码通常用于“服务器收到客户端通过PUT或者POST请求提交的表示，表示的格式正确，但服务器不懂它什么意思”的情况。实体主体：可以包含一个错误的描述文档。401("Unauthorized") 重要程度：高。客户端试图对一个受保护的资源进行操作，却又没有提供正确的认证证书。客户端提供了错误的证书，或者根本没有提供证书。这里的证书（credential）可以是一个用户名/密码，也可以市一个API key，或者一个认证令牌。客户端常常通过向一个URI发送请求，并查看收到401响应，以获知应该发送哪种证书，以及证书的格式。如果服务器不想让未授权的用户获知某个资源的存在，那么它可以谎报一个404而不是401。这样做的缺点是：客户端需要事先知道服务器接受哪种认证--这将导致HTTP摘要认证无法工作。响应报头：WWW-Authenticate报头描述服务器将接受哪种认证。实体主体：一个错误的描述文档。假如最终用户可通过“在网站上注册”的方式得到证书，那么应提供一个指向该注册页面的链接。402("Payment Required") 重要程度：无。除了它的名字外，HTTP标准没有对该响应的其他方面作任何定义。因为目前还没有用于HTTP的微支付系统，所以它被留作将来使用。尽管如此，若存在一个用于HTTP的微支付系统，那么这些系统将首先出现在web服务领域。如果想按请求向用户收费，而且你与用户之间的关系允许这么做的话，那么或许用得上这个响应代码。注：该书印于2008年403("Forbidden") 重要程度：中等。客户端请求的结构正确，但是服务器不想处理它。这跟证书不正确的情况不同--若证书不正确，应该发送响应代码401。该响应代码常用于一个资源只允许在特定时间段内访问，或者允许特定IP地址的用户访问的情况。403暗示了所请求的资源确实存在。跟401一样，若服务器不想透露此信息，它可以谎报一个404。既然客户端请求的结构正确，那为什么还要把本响应代码放在4XX系列（客户端错误），而不是5XX系列（服务端错误）呢？因为服务器不是根据请求的结构，而是根据请求的其他方面（比如说发出请求的时间）作出的决定的。实体主体：一个描述拒绝原因的文档（可选）。404("Not Found") 重要程度：高。这也许是最广为人知的HTTP响应代码了。404表明服务器无法把客户端请求的URI转换为一个资源。相比之下，410更有用一些。web服务可以通过404响应告诉客户端所请求的URI是空的，然后客户端就可以通过向该URI发送PUT请求来创建一个新资源了。但是404也有可能是用来掩饰403或者401.405("Method Not Allowd") 重要程度：中等。客户端试图使用一个本资源不支持的HTTP方法。例如：一个资源只支持GET方法，但是客户端使用PUT方法访问。响应报头：Allow报头列出本资源支持哪些HTTP方法，例如：Allow：GET，POST406("Not Acceptable") 重要程度：中等。当客户端对表示有太多要求，以至于服务器无法提供满足要求的表示，服务器可以发送这个响应代码。例如：客户端通过Accept头指定媒体类型为application/json+hic，但是服务器只支持application/json。服务器的另一个选择是：忽略客户端挑剔的要求，返回首选表示，并把响应代码设为200。实体主体：一个可选表示的链接列表。407("Proxy Authentication Required") 重要程度：低。只有HTTP代理会发送这个响应代码。它跟401类似，唯一区别在于：这里不是无权访问web服务，而是无权访问代理。跟401一样，可能是因为客户端没有提供证书，也可能是客户端提供的证书不正确或不充分。请求报头：客户端通过使用Proxy-Authorization报头（而不是Authorization）把证书提供给代理。格式跟Authrization一样。响应报头：代理通过Proxy-Authenticate报头（而不是WWW-Authenticate）告诉客户端它接受哪种认证。格式跟WWW-Authenticate一样。408("Reqeust Timeout") 重要程度：低。假如HTTP客户端与服务器建立链接后，却不发送任何请求（或从不发送表明请求结束的空白行），那么服务器最终应该发送一个408响应代码，并关闭此连接。409("Conflict") 重要程度：高。此响应代码表明：你请求的操作会导致服务器的资源处于一种不可能或不一致的状态。例如你试图修改某个用户的用户名，而修改后的用户名与其他存在的用户名冲突了。响应报头：若冲突是因为某个其他资源的存在而引起的，那么应该在Location报头里给出那个资源的URI。实体主体：一个描述冲突的文档，以便客户端可以解决冲突。410("Gone") 重要程度：中等。这个响应代码跟404类似，但它提供的有用信息更多一些。这个响应代码用于服务器知道被请求的URI过去曾指向一个资源，但该资源现在不存在了的情况。服务器不知道该资源的新URI，服务器要是知道该URI的话，它就发送响应代码301.410和310一样，都有暗示客户端不应该再请求该URI的意思，不同之处在于：410只是指出该资源不存在，但没有给出该资源的新URI。RFC2616建议“为短期的推广服务，以及属于个人但不继续在服务端运行的资源”采用410.411("Length Required") 重要程度：低到中等。若HTTP请求包含表示，它应该把Content-Length请求报头的值设为该表示的长度（以字节为单位）。对客户端而言，有时这不太方便（例如，当表示是来自其他来源的字节流时）。所以HTTP并不要求客户端在每个请求中都提供Content-Length报头。但HTTP服务器可以要求客户端必须设置该报头。服务器可以中断任何没有提供Content-Length报头的请求，并要求客户端重新提交包含Content-Length报头的请求。这个响应代码就是用于中断未提供Content-Lenght报头的请求的。假如客户端提供错误的长度，或发送超过长度的表示，服务器可以中断请求并关闭链接，并返回响应代码413。412("Precondition Failed") 重要程度：中等。客户端在请求报头里指定一些前提条件，并要求服务器只有在满足一定条件的情况下才能处理本请求。若服务器不满足这些条件，就返回此响应代码。If-Unmodified-Since是一个常见的前提条件。客户端可以通过PUT请求来修改一个资源，但它要求，仅在自客户端最后一次获取该资源后该资源未被别人修改过才能执行修改操作。若没有这一前提条件，客户端可能会无意识地覆盖别人做的修改，或者导致409的产生。请求报头：若客户但设置了If-Match，If-None-Match或If-Unmodified-Since报头，那就有可能得到这个响应代码。If-None-Match稍微特别一些。若客户端在发送GET或HEAD请求时指定了If-None-Match，并且服务器不满足该前提条件的话，那么响应代码不是412而是304，这是实现条件HTTP GET的基础。若客户端在发送PUT，POST或DELETE请求时指定了If-None-Match,并且服务器不满足该前提条件的话，那么响应代码是412.另外，若客户端指定了If-Match或If-Unmodified-Since(无论采用什么HTTP方法)，而服务器不满足该前提条件的话，响应代码也是412。413("Request Entity Too Large") 重要程度：低到中等。这个响应代码跟411类似，服务器可以用它来中断客户端的请求并关闭连接，而不需要等待请求完成。411用于客户端未指定长度的情况，而413用于客户端发送的表示太大，以至于服务器无法处理。客户端可以先做一个LBYL（look-before-you-leap）请求，以免请求被413中断。若LBYL请求获得响应代码为100，客户端再提交完整的表示。响应报头：如果因为服务器方面临时遇到问题（比如资源不足），而不是因为客户端方面的问题而导致中断请求的话，服务器可以把Retry-After报头的值设为一个日期或一个间隔时间，以秒为单位，以便客户端可以过段时间重试。414("Request-URI Too Long") 重要程度：低。HTTP标准并没有对URI长度作出官方限制，但大部分现有的web服务器都对URI长度有一个上限，而web服务可能也一样。导致URI超长的最常见的原因是：表示数据明明是该放在实体主体里的，但客户端却把它放在了URI里。深度嵌套的数据结构也有可能引起URI过长。415("Unsupported Media Type") 重要程度：中等。当客户端在发送表示时采用了一种服务器无法理解的媒体类型，服务器发送此响应代码。比如说，服务器期望的是XML格式，而客户端发送的确实JSON格式。如果客户端采用的媒体类型正确，但格式有问题，这时最好返回更通用的400。416("Requestd Range Not Satisfiable") 重要程度：低。当客户端所请求的字节范围超出表示的实际大小时，服务器发送此响应代码。例如：你请求一个表示的1-100字节，但该表示总共只用99字节大小。请求报头：仅当原始请求里包含Range报头时，才有可能收到此响应代码。若原始请求提供的是If-Range报头，则不会收到此响应代码。响应报头：服务器应当通过Content-Range报头告诉客户端表示的实际大小。417("Expectation Failed") 重要程度：中等。此响应代码跟100正好相反。当你用LBYL请求来考察服务器是否会接受你的表示时，如果服务器确认会接受你的表示，那么你将获得响应代码100，否则你将获得417。5XX 服务端错误这些响应代码表明服务器端出现错误。一般来说，这些代码意味着服务器处于不能执行客户端请求的状态，此时客户端应稍后重试。有时，服务器能够估计客户端应在多久之后重试。并把该信息放在Retry-After响应报头里。5XX系列响应代码在数量上不如4XX系列多，这不是因为服务器错误的几率小，而是因为没有必要如此详细--对于服务器方面的问题，客户端是无能为力的。500("Internal Server Error") 重要程度：高。这是一个通用的服务器错误响应。对于大多数web框架，如果在执行请求处理代码时遇到了异常，它们就发送此响应代码。501("Not Implemented") 重要程度：低。客户端试图使用一个服务器不支持的HTTP特性。最常见的例子是：客户端试图做一个采用了拓展HTTP方法的请求，而普通web服务器不支持此请求。它跟响应代码405比较相似，405表明客户端所用的方法是一个可识别的方法，但该资源不支持，而501表明服务器根本不能识别该方法。502("Bad Gateway") 重要程度：低。只有HTTP代理会发送这个响应代码。它表明代理方面出现问题，或者代理与上行服务器之间出现问题，而不是上行服务器本身有问题。若代理根本无法访问上行服务器，响应代码将是504。503("Service Unavailable") 重要程度：中等到高。此响应代码表明HTTP服务器正常，只是下层web服务服务不能正常工作。最可能的原因是资源不足：服务器突然收到太多请求，以至于无法全部处理。由于此问题多半由客户端反复发送请求造成，因此HTTP服务器可以选择拒绝接受客户端请求而不是接受它，并发送503响应代码。响应报头：服务器可以通过Retry-After报头告知客户端何时可以重试。504("Gateway Timeout") 重要程度：低。跟502类似，只有HTTP代理会发送此响应代码。此响应代码表明代理无法连接上行服务器。505("HTTP Version Not Supported") 重要程度： 非常低。当服务器不支持客户端试图使用的HTTP版本时发送此响应代码。实体主体：一个描述服务器支持哪些协议的文档。Web服务器HTTP进行通信时，实际上会经过很多的转发程序，例如通过代理、网关和隧道。代理：代理时一种具有转发功能的应用程序，接收由客户端发送的请求并转发至服务器，同时也接收服务器返回的响应给客户端隧道：隧道是在相隔距离较远的客户端和服务器之间进行中转，保证双方通信连接完整网关：严格来说，网关和代理的功能较为相似，区别在于代理连接的是两个或多个使用相同协议的应用程序，而网关连接的则是两个或多个使用不同协议的应用程序。HTTP首部请求首部字段首部字段名 说明 Accept-Ranges 是否接受字节范围请求 Age 推算资源创建经过时间 ETag 资源的匹配信息 Location 令客户端重定向至指定URI Proxy-Authenticate 代理服务器对客户端的认证信息 Retry-After 对再次发起请求的时机要求 Server HTTP 服务器的安装信息 Vary 代理服务器缓存的管理信息 WWW-Authenticate 服务器对客户端的认证信息响应首部字段首部字段名 说明 Accept-Ranges 是否接受字节范围请求 Age 推算资源创建经过时间 ETag 资源的匹配信息 Location 令客户端重定向至指定URI Proxy-Authenticate 代理服务器对客户端的认证信息 Retry-After 对再次发起请求的时机要求 Server HTTP 服务器的安装信息 Vary 代理服务器缓存的管理信息 WWW-Authenticate 服务器对客户端的认证信息实体首部字段首部字段名 说明 Allow 资源可支持的HTTP方法 Content-Encoding 实体主体适用的编码方式 Content-Language 实体主体的自然语言 Content-Length 实体主体的大小（单位：字节） Content-Location 替代对应资源的URI Content-MD5 实体主体的报文摘要 Content-Range 实体主体的位置范围 Content-Type 实体主体的媒体类型 Expires 实体主体过期的日期时间 Last-Modified 资源的最后修改日期时间HTTP安全性HTTP使用明文进行传输，内容可能会被窃听HTTP不会验证通信方的身份，因此有可能遭遇伪装HTTP无法证明报文的完整性，所以有可能在请求传输途中遭遇纂改通过在HTTP协议的基础上，在通信接口使用SSL和TLS协议进行代替，通过使用SSL+TLS可以实现加密、认证以及完整性保护的功能。通常，HTTP直接和TCP进行通信，但当使用SSL是，则演变成先和SSL通信，继而由SSL和TCP进行通信。HTTP功能追加协议WebSocket：即Web浏览器和Web服务器之间的全双工通信标准，一旦Web服务器和客户端之间建立起WebSocket协议的通信连接， 之后所有的通信都将依靠这个协议进行通信，并且通信过程中可互相发送JSON、XML、HTML或图片等任意的数据。通过使用WebSocket，可以实现推送功能，支持由服务器向客户端推送数据的推送功能；可以减少通信量，只要建立了WebSocket连接，就希望一直保持着连接状态（HTTP需要多次的连接），而且WebSocket的首部信息量很小，通信量也相应的减少了。
一、单系统登录机制1、http无状态协议 web应用采用browser/server架构，http作为通信协议。http是无状态协议，浏览器的每一次请求，服务器会独立处理，不与之前或之后的请求产生关联，这个过程用下图说明，三次请求/响应对之间没有任何联系3c91a3bf-25d8-4b1f-8e4a-68535c51aaa8 但这也同时意味着，任何用户都能通过浏览器访问服务器资源，如果想保护服务器的某些资源，必须限制浏览器请求；要限制浏览器请求，必须鉴别浏览器请求，响应合法请求，忽略非法请求；要鉴别浏览器请求，必须清楚浏览器请求状态。既然http协议无状态，那就让服务器和浏览器共同维护一个状态吧！这就是会话机制2、会话机制 浏览器第一次请求服务器，服务器创建一个会话，并将会话的id作为响应的一部分发送给浏览器，浏览器存储会话id，并在后续第二次和第三次请求中带上会话id，服务器取得请求中的会话id就知道是不是同一个用户了，这个过程用下图说明，后续请求与第一次请求产生了关联8a9fb230-d506-4b19-b821-4001c68c4588 服务器在内存中保存会话对象，浏览器怎么保存会话id呢？你可能会想到两种方式请求参数cookie 将会话id作为每一个请求的参数，服务器接收请求自然能解析参数获得会话id，并借此判断是否来自同一会话，很明显，这种方式不靠谱。那就浏览器自己来维护这个会话id吧，每次发送http请求时浏览器自动发送会话id，cookie机制正好用来做这件事。cookie是浏览器用来存储少量数据的一种机制，数据以”key/value“形式存储，浏览器发送http请求时自动附带cookie信息 tomcat会话机制当然也实现了cookie，访问tomcat服务器时，浏览器中可以看到一个名为“JSESSIONID”的cookie，这就是tomcat会话机制维护的会话id，使用了cookie的请求响应过程如下图518293d9-64b2-459c-9d45-9f353c757d1f3、登录状态 有了会话机制，登录状态就好明白了，我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份，服务器拿到用户名密码去数据库比对，正确的话说明当前持有这个会话的用户是合法用户，应该将这个会话标记为“已授权”或者“已登录”等等之类的状态，既然是会话的状态，自然要保存在会话对象中，tomcat在会话对象中设置登录状态如下用户再次访问时，tomcat在会话对象中查看登录状态HttpSession session = request.getSession();session.setAttribute("isLogin", true);HttpSession session = request.getSession();session.getAttribute("isLogin"); 实现了登录状态的浏览器请求服务器模型如下图描述 每次请求受保护资源时都会检查会话对象中的登录状态，只有 isLogin=true 的会话才能访问，登录机制因此而实现。二、多系统的复杂性 web系统早已从久远的单系统发展成为如今由多系统组成的应用群，面对如此众多的系统，用户难道要一个一个登录、然后一个一个注销吗？就像下图描述的这样6dfbb0b1-46c0-4945-a3bf-5f060fa80710 web系统由单系统发展成多系统组成的应用群，复杂性应该由系统内部承担，而不是用户。无论web系统内部多么复杂，对用户而言，都是一个统一的整体，也就是说，用户访问web系统的整个应用群与访问单个系统一样，登录/注销只要一次就够了9fe14ab3-4254-447b-b850-0436e628c254 虽然单系统的登录解决方案很完美，但对于多系统应用群已经不再适用了，为什么呢？ 单系统登录解决方案的核心是cookie，cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的，这个限制就是cookie的域（通常对应网站的域名），浏览器发送http请求时会自动携带与该域匹配的cookie，而不是所有cookie4d58ccfa-0114-486d-bec2-c28f2f9eb513 既然这样，为什么不将web应用群中所有子系统的域名统一在一个顶级域名下，例如“*.baidu.com”，然后将它们的cookie域设置为“baidu.com”，这种做法理论上是可以的，甚至早期很多多系统登录就采用这种同域名共享cookie的方式。 然而，可行并不代表好，共享cookie的方式存在众多局限。首先，应用群域名得统一；其次，应用群各系统使用的技术（至少是web服务器）要相同，不然cookie的key值（tomcat为JSESSIONID）不同，无法维持会话，共享cookie的方式是无法实现跨语言技术平台登录的，比如java、php、.net系统之间；第三，cookie本身不安全。 因此，我们需要一种全新的登录方式来实现多系统应用群的登录，这就是单点登录三、单点登录 什么是单点登录？单点登录全称Single Sign On（以下简称SSO），是指在多系统应用群中登录一个系统，便可在其他所有系统中得到授权而无需再次登录，包括单点登录与单点注销两部分1、登录 相比于单系统登录，sso需要一个独立的认证中心，只有认证中心能接受用户的用户名密码等安全信息，其他系统不提供登录入口，只接受认证中心的间接授权。间接授权通过令牌实现，sso认证中心验证用户的用户名密码没问题，创建授权令牌，在接下来的跳转过程中，授权令牌作为参数发送给各个子系统，子系统拿到令牌，即得到了授权，可以借此创建局部会话，局部会话登录方式与单系统的登录方式相同。这个过程，也就是单点登录的原理，用下图说明 下面对上图简要描述用户访问系统1的受保护资源，系统1发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数sso认证中心发现用户未登录，将用户引导至登录页面用户输入用户名密码提交登录申请sso认证中心校验用户信息，创建用户与sso认证中心之间的会话，称为全局会话，同时创建授权令牌sso认证中心带着令牌跳转会最初的请求地址（系统1）系统1拿到令牌，去sso认证中心校验令牌是否有效sso认证中心校验令牌，返回有效，注册系统1系统1使用该令牌创建与用户的会话，称为局部会话，返回受保护资源用户访问系统2的受保护资源系统2发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数sso认证中心发现用户已登录，跳转回系统2的地址，并附上令牌系统2拿到令牌，去sso认证中心校验令牌是否有效sso认证中心校验令牌，返回有效，注册系统2系统2使用该令牌创建与用户的局部会话，返回受保护资源 用户登录成功之后，会与sso认证中心及各个子系统建立会话，用户与sso认证中心建立的会话称为全局会话，用户与各个子系统建立的会话称为局部会话，局部会话建立之后，用户访问子系统受保护资源将不再通过sso认证中心，全局会话与局部会话有如下约束关系局部会话存在，全局会话一定存在全局会话存在，局部会话不一定存在全局会话销毁，局部会话必须销毁 你可以通过博客园、百度、csdn、淘宝等网站的登录过程加深对单点登录的理解，注意观察登录过程中的跳转url与参数2、注销 单点登录自然也要单点注销，在一个子系统中注销，所有子系统的会话都将被销毁，用下面的图来说明3b139d2e-0b83-4a69-b4f2-316adb8997ce sso认证中心一直监听全局会话的状态，一旦全局会话销毁，监听器将通知所有注册系统执行注销操作 下面对上图简要说明用户向系统1发起注销请求系统1根据用户与系统1建立的会话id拿到令牌，向sso认证中心发起注销请求sso认证中心校验令牌有效，销毁全局会话，同时取出所有用此令牌注册的系统地址sso认证中心向所有注册系统发起注销请求各注册系统接收sso认证中心的注销请求，销毁局部会话sso认证中心引导用户至登录页面四、部署图 单点登录涉及sso认证中心与众子系统，子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求，因而子系统必须集成sso的客户端，sso认证中心则是sso服务端，整个单点登录过程实质是sso客户端与服务端通信的过程，用下图描述fb29685c-487c-42b9-9ceb-6c7ee29e98c9 sso认证中心与sso客户端通信方式有多种，这里以简单好用的httpClient为例，web service、rpc、restful api都可以五、实现 只是简要介绍下基于java的实现过程，不提供完整源码，明白了原理，我相信你们可以自己实现。sso采用客户端/服务端架构，我们先看sso-client与sso-server要实现的功能（下面：sso认证中心=sso-server） sso-client拦截子系统未登录用户请求，跳转至sso认证中心接收并存储sso认证中心发送的令牌与sso-server通信，校验令牌的有效性建立局部会话拦截用户注销请求，向sso认证中心发送注销请求接收sso认证中心发出的注销请求，销毁局部会话 sso-server验证用户的登录信息创建全局会话创建授权令牌与sso-client通信发送令牌校验sso-client令牌有效性系统注册接收sso-client注销请求，注销所有会话 接下来，我们按照原理来一步步实现sso吧！1、sso-client拦截未登录请求 java拦截请求的方式有servlet、filter、listener三种方式，我们采用filter。在sso-client中新建LoginFilter.java类并实现Filter接口，在doFilter()方法中加入对未登录用户的拦截public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; HttpSession session = req.getSession(); if (session.getAttribute("isLogin")) { chain.doFilter(request, response); return; } //跳转至sso认证中心 res.sendRedirect("sso-server-url-with-system-url"); }2、sso-server拦截未登录请求 拦截从sso-client跳转至sso认证中心的未登录请求，跳转至登录页面，这个过程与sso-client完全一样3、sso-server验证用户登录信息 用户在登录页面输入用户名密码，请求登录，sso认证中心校验用户信息，校验成功，将会话状态标记为“已登录”@RequestMapping("/login") public String login(String username, String password, HttpServletRequest req) { this.checkLoginInfo(username, password); req.getSession().setAttribute("isLogin", true); return "success"; }4、sso-server创建授权令牌 授权令牌是一串随机字符，以什么样的方式生成都没有关系，只要不重复、不易伪造即可，下面是一个例子String token = UUID.randomUUID().toString();5、sso-client取得令牌并校验 sso认证中心登录后，跳转回子系统并附上令牌，子系统（sso-client）取得令牌，然后去sso认证中心校验，在LoginFilter.java的doFilter()中添加几行 // 请求附带token参数 String token = req.getParameter("token"); if (token != null) { // 去sso认证中心校验token boolean verifyResult = this.verify("sso-server-verify-url", token); if (!verifyResult) { res.sendRedirect("sso-server-url"); return; } chain.doFilter(request, response); } verify()方法使用httpClient实现，这里仅简略介绍，httpClient详细使用方法请参考官方文档HttpPost httpPost = new HttpPost("sso-server-verify-url-with-token"); HttpResponse httpResponse = httpClient.execute(httpPost);6、sso-server接收并处理校验令牌请求 用户在sso认证中心登录成功后，sso-server创建授权令牌并存储该令牌，所以，sso-server对令牌的校验就是去查找这个令牌是否存在以及是否过期，令牌校验成功后sso-server将发送校验请求的系统注册到sso认证中心（就是存储起来的意思） 令牌与注册系统地址通常存储在key-value数据库（如redis）中，redis可以为key设置有效时间也就是令牌的有效期。redis运行在内存中，速度非常快，正好sso-server不需要持久化任何数据。 令牌与注册系统地址可以用下图描述的结构存储在redis中，可能你会问，为什么要存储这些系统的地址？如果不存储，注销的时候就麻烦了，用户向sso认证中心提交注销请求，sso认证中心注销全局会话，但不知道哪些系统用此全局会话建立了自己的局部会话，也不知道要向哪些子系统发送注销请求注销局部会话3b221593-f9c4-45af-a567-4937786993e87、sso-client校验令牌成功创建局部会话 令牌校验成功后，sso-client将当前局部会话标记为“已登录”，修改LoginFilter.java，添加几行if (verifyResult) { session.setAttribute("isLogin", true); } sso-client还需将当前会话id与令牌绑定，表示这个会话的登录状态与令牌相关，此关系可以用java的hashmap保存，保存的数据用来处理sso认证中心发来的注销请求8、注销过程 用户向子系统发送带有“logout”参数的请求（注销请求），sso-client拦截器拦截该请求，向sso认证中心发起注销请求String logout = req.getParameter("logout"); if (logout != null) { this.ssoServer.logout(token); } sso认证中心也用同样的方式识别出sso-client的请求是注销请求（带有“logout”参数），sso认证中心注销全局会话@RequestMapping("/logout") public String logout(HttpServletRequest req) { HttpSession session = req.getSession(); if (session != null) { session.invalidate();//触发LogoutListener } return "redirect:/"; } sso认证中心有一个全局会话的监听器，一旦全局会话注销，将通知所有注册系统注销public class LogoutListener implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent event) {} @Override public void sessionDestroyed(HttpSessionEvent event) { //通过httpClient向所有注册系统发送注销请求 } }
MockMvc详解MockMvc是什么简单来说，对于web应用来讲，你需要对你的各个模块进行测试，但是通过启动项目使用postman来测试的话比较麻烦（程序启动速度慢，测试验证不方便，依赖网络环境），那么为了使我们的单元测试更加简单、高效，我们引入了MockMvcMockMvc实现了对于Http请求的模拟，能够直接使用网络的形式，转换到contoller的调用，可以使得测试速度快、不依赖网络环境，而且还提供了专门的一套验证的工具。测试的逻辑MockMvcBuilder构造MockMvc的构造器MockMvc调用perform，执行一个RequestBuilder请求，调用controller的业务处理逻辑perform返回ResultActions,返回操作的结果，通过ResultActions，提供了统一的验证方式使用StatusResultMatchers对请求结果进行验证使用ContentResultMatchers对请求的内容进行验证如何获取mockMvc实例两个获取mockMvc实例的方式：StandaloneMockMvcBuilderDefaultMockMvcBuilder具体实现方式：MockMvcBuilders.webAppContextSetup(WebApplicationContext context)指定WebApplicationContext，将会从该上下文获取相应的控制器并得到相应的MockMvcMockMvcBuilders.standaloneSetup(Object... controllers)通过参数指定一组控制器，这样就不需要从上下文获取了比如this.mockMvc = MockMvcBuilders.standaloneSetup(this.controller).build();## 构造请求——MockMvcRequestBuilders构造请求示例：MockMvcRequestBuilders.get("/user")MockMvcRequestBuilders主要APIMockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables)：根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder；如get(/user/{id}, 1L)； MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables)：同get类似，但是是POST方法； MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables)：同get类似，但是是PUT方法； MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) ：同get类似，但是是DELETE方法； MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables)：同get类似，但是是OPTIONS方法； MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables)： 提供自己的Http请求方法及uri模板和uri变量，如上API都是委托给这个API； MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables)：提供文件上传方式的请求，得到MockMultipartHttpServletRequestBuilder； RequestBuilder asyncDispatch(final MvcResult mvcResult)：创建一个从启动异步处理的请求的MvcResult进行异步分派的RequestBuilder； 指定请求参数——MockHttpServletRequestBuilder当然，我们不仅可以设置它的请求方式，同样也可以设置它的请求参数MockHttpServletRequestBuilder主要APIMockHttpServletRequestBuilder header(String name, Object... values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders)：添加头信息； MockHttpServletRequestBuilder contentType(MediaType mediaType)：指定请求的contentType头信息； MockHttpServletRequestBuilder accept(MediaType... mediaTypes)/MockHttpServletRequestBuilder accept(String... mediaTypes)：指定请求的Accept头信息； MockHttpServletRequestBuilder content(byte[] content)/MockHttpServletRequestBuilder content(String content)：指定请求Body体内容； MockHttpServletRequestBuilder cookie(Cookie... cookies)：指定请求的Cookie； MockHttpServletRequestBuilder locale(Locale locale)：指定请求的Locale； MockHttpServletRequestBuilder characterEncoding(String encoding)：指定请求字符编码； MockHttpServletRequestBuilder requestAttr(String name, Object value) ：设置请求属性数据； MockHttpServletRequestBuilder sessionAttr(String name, Object value)/MockHttpServletRequestBuilder sessionAttrs(Map<string, object=""> sessionAttributes)：设置请求session属性数据； MockHttpServletRequestBuilder flashAttr(String name, Object value)/MockHttpServletRequestBuilder flashAttrs(Map<string, object=""> flashAttributes)：指定请求的flash信息，比如重定向后的属性信息； MockHttpServletRequestBuilder session(MockHttpSession session) ：指定请求的Session； MockHttpServletRequestBuilder principal(Principal principal) ：指定请求的Principal； MockHttpServletRequestBuilder contextPath(String contextPath) ：指定请求的上下文路径，必须以“/”开头，且不能以“/”结尾； MockHttpServletRequestBuilder pathInfo(String pathInfo) ：请求的路径信息，必须以“/”开头； MockHttpServletRequestBuilder secure(boolean secure)：请求是否使用安全通道； MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor)：请求的后处理器，用于自定义一些请求处理的扩展点；ResultActions在请求体全部构建完成之后，我们就要针对响应的内容做断言，来验证我们测试的有效性通过调用MockMvc.perform()之后会返回一个ResultActions对象，通过ResultActions我们可以做以下三件事：ResultActions andExpect(ResultMatcher matcher) ：添加验证断言来判断执行请求后的结果是否是预期的；ResultActions andDo(ResultHandler handler) ：添加结果处理器，用于对验证成功后执行的动作，如输出下请求/结果信息用于调试；MvcResult andReturn() ：返回验证成功后的MvcResult；用于自定义验证/下一步的异步处理；
代码层级梳理代码层级annotation层ExcelVoAttribut主要用于写@ExcelVoAttribute注解，可以更灵活的配置Excel数据表导出的一些基本配置LogDirection主要用于写@LogDirection注解，注解里包含一个字段（接口描述），通过接口添加这个注解，填写上接口的功能，当用户在访问接口时候，可以利用aop技术，把用户的操作记到日志里aspect层LogDrectionAspect主要定义了aop切点的位置和增强的方法/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Aspect @Component @Slf4j public class LogDirectionAspect { @Pointcut("@annotation(logDirection)") public void doLogDirection(LogDirection logDirection){ } @Around(value = "doLogDirection(logDirection)",argNames = "pjp,logDirection") public Object doBefore(ProceedingJoinPoint pjp,LogDirection logDirection) throws Throwable{ if (!"".equals(logDirection.direction())) { log.info(logDirection.direction()+"接口被调用"); return pjp.proceed(); } return "接口信息不规范"; } }@Pointcut定义切点，此处定义切点为使用@LogDirection的方法@Around定义增强的方法，通过around的参数ProceedingJoinPoint可以很好的控制程序能不能继续执行下一步，在权限判定发挥着比较重要的作用拓展：利用下面的这种方法，需要在xml文件中配置bean实例，当然也可以在方法中通过手动扫包的方法获得bean实例进行操作/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Aspect public class MyAdvice { /** * 配置切点 */ @Pointcut("execution(* com.soft1851.spring.web.dao..*.insert*(..))") public void pointCut() { } /** * 配置前置增强 */ @Before("MyAdvice.pointCut()") public void beforeMethod() { System.out.println("等待数据插入"); } /** * 配置后置增强 */ @AfterReturning("execution(* com.soft1851.spring.web.dao..*.*(..))") public void afterMethod(){ System.out.println("关闭数据库"); } }xml配置实例 <bean name="forumDao" class="com.soft1851.spring.web.dao.impl.ForumDaoImpl"/> <bean name="myAdvice" class="com.soft1851.spring.web.interceptor.MyAdvice"/> <aop:aspectj-autoproxy/>common层ResponseResult和ResultCode这两个类可以将响应体统一进行封装，将接口的响应体规范化，使前端获取数据也更加友好贴下代码，防止以后找不到ResponseResult类package com.soft1851.music.admin.common; import lombok.Data; import java.io.Serializable; /** * @author Johnny * @Date: 2020/4/21 19:52 * @Description: */ @Data public class ResponseResult implements Serializable { private static final long serialVersionUID = -3948389268046368059L; private Integer code; private String msg; private Object data; private ResponseResult() { } public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public static ResponseResult success() { ResponseResult result = new ResponseResult(); result.setResultCode(ResultCode.SUCCESS); return result; } public static ResponseResult success(Object data) { ResponseResult result = new ResponseResult(); result.setResultCode(ResultCode.SUCCESS); result.setData(data); return result; } public static ResponseResult failure(ResultCode resultCode) { ResponseResult result = new ResponseResult(); result.setResultCode(resultCode); return result; } public static ResponseResult failure(ResultCode resultCode, Object data) { ResponseResult result = new ResponseResult(); result.setResultCode(resultCode); result.setData(data); return result; } public void setResultCode(ResultCode code) { this.code = code.code(); this.msg = code.message(); } }ResultCode类（枚举）package com.soft1851.music.admin.common; /** * @author Johnny * @Date: 2020/4/21 19:52 * @Description: */ public enum ResultCode { /* 成功状态码 */ SUCCESS(1, "成功"), /* 通用错误：10001-19999 */ PARAM_IS_INVALID(10001, "参数无效"), PARAM_IS_BLANK(10002, "参数为空"), PARAM_TYPE_BIND_ERROR(10003, "参数类型错误"), PARAM_NOT_COMPLETE(10004, "参数缺失"), HTTP_METHOD_ERROR(10005, "请求方法错误"), HTTP_METHOD_NOT_ALLOWED(10006, "请求方法不允许"), HTTP_NOT_FOUND(10007, "请求地址错误"), BOUND_STATEMENT_NOT_FOUND(10008, "Mybatis未绑定"), CONNECTION_ERROR(10009, "网络连接错误"), ARITHMETIC_ERROR(100010, "计算错误"), /* 用户错误：20001-29999*/ USER_NOT_SIGN_IN(20001, "请先登录"), USER_PASSWORD_ERROR(20002, "密码错误"), USER_ACCOUNT_ERROR(20003, "账号错误"), USER_VERIFY_CODE_ERROR(20004, "验证码错误"), USER_CODE_TIMEOUT(20005, "验证码失效"), USER_ACCOUNT_FORBIDDEN(20006, "账号已被禁用"), USER_SIGN_UP_FAIL(20007, "用户注册失败"), USER_SIGN_IN_FAIL(20008, "用户登录失败"), USER_NOT_FOUND(20009, "用户不存在"), USER_NO_AUTH(20019, "用户权限不足"), USER_TOKEN_EXPIRES(200010, "Token已过期"), /* 业务错误：30001-39999 */ SMS_ERROR(30001, "短信业务出现问题"), UPLOAD_ERROR(30002, "上传文件业务出现问题"), CAPTCHA_ERROR(30003, "验证码业务出现问题"), /* 数据错误：40001-49999 */ RESULT_CODE_DATA_NONE(50001, "数据未找到"), DATA_IS_WRONG(50002, "数据有误"), DATA_ALREADY_EXISTED(50003, "数据已存在"), DATABASE_ERROR(50004, "数据库操作异常"), /* 服务器或系统错误：50001-599999 */ SERVER_ERROR(50000, "服务器错误，请稍后重试"), SYSTEM_ERROR(40001, "系统错误，请稍后重试"), /* 接口错误：60001-69999 */ INTERFACE_INNER_INVOKE_ERROR(60001, "内部系统接口调用异常"), INTERFACE_OUTER_INVOKE_ERROR(60002, "外部系统接口调用异常"), INTERFACE_FORBID_VISIT(60003, "该接口禁止访问"), INTERFACE_ADDRESS_INVALID(60004, "接口地址无效"), INTERFACE_REQUEST_TIMEOUT(60005, "接口请求超时"), INTERFACE_EXCEED_LOAD(60006, "接口负载过高"), /* 权限错误：70001-79999 */ PERMISSION_NO_ACCESS(70001,"无访问权限"); private Integer code; private String message; ResultCode(Integer code, String message) { this.code = code; this.message = message; } public Integer code() { return this.code; } public String message() { return this.message; } public static String getMessage(String name) { for (ResultCode item : ResultCode.values()) { if (item.name().equals(name)) { return item.message; } } return name; } public static Integer getCode(String name) { for (ResultCode item : ResultCode.values()) { if (item.name().equals(name)) { return item.code; } } return null; } @Override public String toString() { return this.name(); } }config层CaptchaConfig主要是为kaptcha谷歌验证码的一些基本信息进行配置，比如字体，验证码位数等等附上一些kaptha的基本配置信息Constant描述默认值kaptcha.border图片边框，合法值：yes , noyeskaptcha.border.color边框颜色，合法值： r,g,b (and optional alpha) 或者 white,black,blue.blackkaptcha.border.thickness边框厚度，合法值：>01kaptcha.image.width图片宽200kaptcha.image.height图片高50kaptcha.producer.impl图片实现类com.google.code.kaptcha.impl.DefaultKaptchakaptcha.textproducer.impl文本实现类com.google.code.kaptcha.text.impl.DefaultTextCreatorkaptcha.textproducer.char.string文本集合，验证码值从此集合中获取abcde2345678gfynmnpwxkaptcha.textproducer.char.length验证码长度5kaptcha.textproducer.font.names字体Arial, Courierkaptcha.textproducer.font.size字体大小40px.kaptcha.textproducer.font.color字体颜色，合法值： r,g,b 或者 white,black,blue.blackkaptcha.textproducer.char.space文字间隔2kaptcha.noise.impl干扰实现类com.google.code.kaptcha.impl.DefaultNoisekaptcha.noise.color干扰 颜色，合法值： r,g,b 或者 white,black,blue.blackkaptcha.obscurificator.impl图片样式： 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpycom.google.code.kaptcha.impl.WaterRipplekaptcha.background.impl背景实现类com.google.code.kaptcha.impl.DefaultBackgroundkaptcha.background.clear.from背景颜色渐变，开始颜色light greykaptcha.background.clear.to背景颜色渐变， 结束颜色whitekaptcha.word.impl文字渲染器com.google.code.kaptcha.text.impl.DefaultWordRendererkaptcha.session.keysession keyKAPTCHA_SESSION_KEYkaptcha.session.datesession dateKAPTCHA_SESSION_DATECorsConfig主要是为跨域做一些基本的配置，学习阶段后端必备代码展示，防止以后忘记/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Configuration public class CorsConfig { @Bean public FilterRegistrationBean<CorsFilter> corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); //放行所有跨域的客户端domain config.addAllowedOrigin("*"); //允许的请求方法列表 String[] requestMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}; List<String> allowedRequestMethods = Arrays.asList(requestMethods); config.setAllowedMethods(allowedRequestMethods); //允许的客户端请求头列表 String[] requestHeaders = {"x-requested-with", "Content-Type", "Authorization"}; List<String> allowedHeaders = Arrays.asList(requestHeaders); config.setAllowedHeaders(allowedHeaders); //允许的响应头列表 String[] responseHeaders = {"Access-Control-Expose-Headers", "Authorization"}; List<String> allowedExposedHeaders = Arrays.asList(responseHeaders); config.setExposedHeaders(allowedExposedHeaders); source.registerCorsConfiguration("/**", config); FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source)); // 这个顺序很重要,设置在最前 bean.setOrder(0); return bean; } }MybatisPlusConfig主要是为mybatisplus做一些基本配置（如果要有分页查询数据，则这个配置文件必须要有）/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @EnableTransactionManagement @Configuration @MapperScan("com.baomidou.cloud.service.*.mapper*") public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); //设置请求的页面大于最大页操作，true调回到首页，false继续请求，默认为false paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true)); return paginationInterceptor; } }MySqlGenerator代码生成器，功能强大无比，根据数据表，一键生成各种层级，各种实体/** * @ClassName MySqlGenerator * @Description 代码生成器 * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Slf4j public class MySqlGenerator { public static void main(String[] args) { //全局策略配置 GlobalConfig config = new GlobalConfig(); //获得当前项目根路径d:/dev/SpringBoot/spring-boot-learning String projectPath = System.getProperty("user.dir"); config.setActiveRecord(true) //作者注释 .setAuthor("Johnny") //代码生成输出路径 .setOutputDir(projectPath + "/src/main/java") //覆盖已有文件，默认false .setFileOverride(true) //是否打开输出目录窗口。默认true .setOpen(false) //开启swagger2模式 //.setSwagger2(true) //开启ActiveRecord模式 .setActiveRecord(true) //mapper添加restMap .setBaseResultMap(true) //mapper添加Base_Column_List .setBaseColumnList(true) //时间类型对应策略，默认time_pack //.setDateType(DateType.TIME_PACK) //相关包中的接口和类名后缀 .setMapperName("%sMapper") .setServiceName("%sService") .setServiceImplName("%sServiceImpl"); //数据库表配置，通过该配置，可指定需要生成哪些表或者排除哪些表 StrategyConfig strategyConfig = new StrategyConfig(); //是否大写命名 strategyConfig.setCapitalMode(true) //是否跳过视图 .setSkipView(true) //数据库表映射到实体的命名策略为驼峰式 .setNaming(NamingStrategy.underline_to_camel) //生成表，可以写多个,如果不加参数，默认为所有表 .setInclude() .setEntityBuilderModel(true) .setEntityLombokModel(true) .setRestControllerStyle(true) .setEntityTableFieldAnnotationEnable(true); //包名配置 PackageConfig packageConfig = new PackageConfig(); //父包名 packageConfig.setParent("com.soft1851.music.admin") .setMapper("mapper") .setService("service") .setController("controller") .setXml("xml") .setEntity("entity"); //数据源配置 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setDbType(DbType.MYSQL) .setDriverName("com.mysql.jdbc.Driver") .setUrl("jdbc:mysql://rm-m5ee476bu350735gjeo.mysql.rds.aliyuncs.com:3306/cloud_music?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true") .setUsername("root") .setPassword("XuNiit_#"); AutoGenerator autoGenerator = new AutoGenerator(); autoGenerator.setGlobalConfig(config) .setStrategy(strategyConfig) .setDataSource(dataSourceConfig) .setTemplateEngine(new FreemarkerTemplateEngine()) .setPackageInfo(packageConfig); autoGenerator.execute(); log.info("=============代码生成成功================"); } }RedisConfig主要对RedisConfig缓存数据库进行一些基本配置/** * @author Johnny * @Date: 2020/4/21 20:45 * @Description: */ @Configuration @EnableCaching public class RedisConfig { @Bean CacheManager cacheManager(RedisConnectionFactory connectionFactory) { //初始化RedisCacheWrite RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig(); //设置默认过期时间为1天 defaultCacheConfig.entryTtl(Duration.ofDays(1)); //初始化RedisCacheManger return new RedisCacheManager(redisCacheWriter, defaultCacheConfig); } @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object,Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值（默认使用JDK的序列化方式） Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); //使用stringRedisSerializer来序列化和反序列化redis的value值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) { StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); stringRedisTemplate.setConnectionFactory(factory); return stringRedisTemplate; } }WebConfig这个类中主要配置了拦截器，可以对部分页面进行统一拦截，当然这个只是统一的拦截，具体的拦截所要进行的操作，还在下文/** * @author Johnny * @Date: 2020/4/21 20:58 * @Description: */ @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Resource private JwtInterceptor jwtInterceptor; /** * 添加拦截器配置 */ @Override public void addInterceptors(InterceptorRegistry registry) { //拦截路径可自行配置多个 可用 ，分隔开 registry.addInterceptor(loginInterceptor).addPathPatterns("/sysAdmin/login").excludePathPatterns("/**").excludePathPatterns("/static/**"); // registry.addInterceptor(jwtInterceptor).addPathPatterns("/**").excludePathPatterns("/sysAdmin/login", "/captcha","/songList/page").excludePathPatterns("/static/**"); } }controller层controller层暂时带过，后面有时间的话把controller层常用注解汇总整理下dto层目前还没有将entity、vo、dto统一放到domain里，dto里主要是用户请求接口时需要传的参数，vo主要时后端返回前端展示的参数，而entity就是我们和数据库表所吻合的类。举例：我们数据库中的用户表的字段肯定不只时账号和密码，会有一些其他字段，比如加密盐、用户基本信息等等，但是用户请求登录接口时，只需要传账号和密码就可以了，此时此刻，为了防止资源的浪费，我们就定义一个dto来接收前端传来的参数。当用户进入主界面时，查看个人信息，后端并不需要将用户id、加密盐等私密信息展示到前端，此时，同样为了节省资源，我们定义vo视图对象，将前端需要的参数放进去，再传入前端。entity层entity层主要是将数据库中的表一对一的写好，此处是直接用代码生成器生成exceptionCustomException定义全局异常处理，使用时直接throws CustomException就好/** * @author Johnny * @Date: 2020/4/22 10:48 * @Description: */ public class CustomException extends RuntimeException { protected ResultCode resultCode; public CustomException(String msg, ResultCode resultCode) { super(msg); this.resultCode = resultCode; } public ResultCode getResultCode() { return resultCode; } }GlobalException也是全局异常处理，但和上面略有区别，非手动throw，spring在遇到异常时，会自动来这个类里查找，如果已经有配置过的，就会按照配置要求输出内容/** * @author Johnny * @Date: 2020/4/30 18:45 * @Description: */ @ControllerAdvice public class GlobalException { @ExceptionHandler(ConstraintViolationException.class) ResponseResult handleConstraintViolationException(ConstraintViolationException e) { return ResponseResult.failure(ResultCode.DATA_IS_WRONG); } }JwtException同CustomException类格式一样，但这个侧重于处理Jwt模块的异常filterChannelFilter/** * @author Johnny * @Date: 2020/4/21 20:19 * @Description: */ @WebFilter(urlPatterns = "/*",filterName = "channelFilter") public class ChannelFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (servletRequest instanceof HttpServletRequest) { requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest); } if (requestWrapper == null) { filterChain.doFilter(servletRequest,servletResponse); }else{ filterChain.doFilter(requestWrapper, servletResponse); } } @Override public void destroy() { } }LoginFilter这个类主要针对用户登录进行过滤/** * @author Johnny * @Date: 2020/4/26 10:46 * @Description: */ @Slf4j @WebFilter(urlPatterns = "/login",filterName = "LoginFilter") public class LoginFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (servletRequest instanceof HttpServletRequest) { String url = ((HttpServletRequest) servletRequest).getRequestURI(); //判断接口是否为导入接口 if ("/resource/guide".equals(url)) { Part file = ((HttpServletRequest) servletRequest).getPart("file"); log.info("文件名：" + file); } requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest); } if (requestWrapper == null) { filterChain.doFilter(servletRequest, servletResponse); }else{ filterChain.doFilter(requestWrapper, servletResponse); } } @Override public void destroy() { } }handle层GlobalExceptionHandle定义全局异常/** * @author Johnny * @Date: 2020/4/22 10:40 * @Description: */ @RestControllerAdvice(annotations = {RestController.class, Controller.class}) @Slf4j public class GlobalExceptionHandle { //自定义异常 /** * JwtException * @param exception * @return */ @ExceptionHandler(value = {JwtException.class}) @ResponseBody public ResponseResult sendError(JwtException exception) { log.error(exception.getMessage()); return ResponseResult.failure(exception.getResultCode()); } /** * CustomException * @param exception * @return */ @ExceptionHandler(value = {CustomException.class}) @ResponseBody public ResponseResult sendError(CustomException exception) { log.error(exception.getMessage()); return ResponseResult.failure(exception.getResultCode()); } //系统级异常 /** * InvalidClassException * @param exception * @return */ @ExceptionHandler(value = {InvalidClassException.class}) @ResponseBody public ResponseResult sendError(InvalidClaimException exception) { log.error(exception.getMessage()); //返回token已经过期 return ResponseResult.failure(ResultCode.USER_TOKEN_EXPIRES); } /** * NullPointerException * @param exception * @return */ @ExceptionHandler(value = {NullPointerException.class}) @ResponseBody public ResponseResult sendError(NullPointerException exception) { log.error(exception.getMessage()); //空指针提示数据未找到 return ResponseResult.failure(ResultCode.RESULT_CODE_DATA_NONE); } /** * IOException * @param exception * @return */ @ExceptionHandler(value = {IOException.class}) @ResponseBody public ResponseResult sendError(IOException exception) { log.error(exception.getMessage()); //io异常提示验证码出现问题 return ResponseResult.failure(ResultCode.CAPTCHA_ERROR); }GlobalResponseHandle处理全局的响应头（当接口返回类型非ResponseResult类型时，可以自动转换）/** * @author Johnny * @Date: 2020/4/22 10:55 * @Description: */ @ControllerAdvice public class GlobalResponseHandle implements ResponseBodyAdvice { /** * 处理响应的具体方法 * @param returnType * @param converterType * @return */ @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof ResponseResult) { return body; }else{ return ResponseResult.success(body); } } }interceptorJwtInterceptor/** * @author Johnny * @Date: 2020/4/23 19:41 * @Description: */ @Slf4j @Component public class JwtInterceptor implements HandlerInterceptor { @Resource SysRoleService roleService; @Resource private RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); log.info("token———————————"+token); //认证 if (token == null) { log.info("用户未登录"); return false; }else{ //登录之后 //从请求头当中取出用户id String adminId = request.getHeader("id"); log.info("用户id" + adminId); //在redis中检查是否存在adminId为key的数据 if (!redisService.existsKey(adminId)) { log.info("redis中不存在此键"); }else{ String secrect = redisService.getValue(adminId, String.class); //从token中解析出roles字符串 String roles = JwtTokenUtil.getRoles(token,secrect); //反序列化成List List<SysRole> sysRoles = JSONArray.parseArray(roles, SysRole.class); //从request中取得客户端传输的roleId String roleId = request.getParameter("roleId"); log.info("roleId:"+roleId); boolean flag = roleService.checkRole(sysRoles,Integer.parseInt(roleId)); if(flag){ return true; }else{ log.info("用户不具备此角色"); } } return false; } } }LoginInterceptor/** * @author Johnny * @Date: 2020/4/21 21:00 * @Description: */ @Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { @Resource private RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle) { //将request包装成HttpServletWrapper类型 RequestWrapper requestWrapper = new RequestWrapper(request); //取得请求的json对象 String body = requestWrapper.getBody(); log.info(body); //从redis中取得指定用户名的验证码 JSONObject jsonObject = JSONObject.parseObject(body); String name = jsonObject.getString("name"); System.out.println(name); String password = jsonObject.getString("password"); System.out.println(password); String verifyCode = jsonObject.getString("verifyCode"); System.out.println(verifyCode); LoginDto loginDto = LoginDto.builder().name(name).password(password).verifyCode(verifyCode).build(); return true; // if (redisService.existsKey(loginDto.getName())) { // //获取redis中的验证码 // String correctCode = redisService.getValue(name, String.class); // //忽略大小写，成功则放行到controller调用登录接口 // if (correctCode.equalsIgnoreCase(verifyCode)) { // return true; // }else{ // log.info("验证码错误"); // return false; // } // }else{ // log.info("验证码失效"); // return false; // } } }mapper层这个目前不做过多赘述，我会尽快整理下mybatisplus的常用语法发出来util层ExcelConsumer/** * @author Johnny * @Date: 2020/4/28 21:53 * @Description: */ @Slf4j public class ExcelConsumer<T> implements Runnable { /** * 一张工作表可以容纳的最大行数 */ private static Integer SHEET_SIZE = 100000; /** * 导出的Vo数据类型 */ private Class<T> clazz; /** * 工作簿 */ private SXSSFWorkbook wb; /** * 工作表名称 */ private String sheetName; /** * 数据缓冲区对象 */ private ExportDataAdapter<T> exportDataAdapter; /** * 线程同步 */ private CountDownLatch latch; /** * 构造方法 * * @param clazz */ public ExcelConsumer(Class<T> clazz, ExportDataAdapter<T> exportDataAdapter, SXSSFWorkbook wb, CountDownLatch latch, String sheetName) { if (clazz == null || wb == null || exportDataAdapter == null || latch == null) { log.error("ExcelConsumer::初始化对象参数不能为空"); return; } this.clazz = clazz; this.exportDataAdapter = exportDataAdapter; this.wb = wb; this.latch = latch; this.sheetName = sheetName == null ? "UnNamedSheet" : sheetName; } @Override public void run() { //初始化excel导出工具类 ExcelUtil<T> excelUtil = new ExcelUtil<>(this.clazz); Sheet sheet = null; int sheetNo = 0; int rowNum = 1; T vo; //生产者还在生产数据 while (latch.getCount() > 1) { //生成sheetName if (rowNum == 1) { sheetNo++; sheet = excelUtil.createSheet(wb, sheetName.concat(Integer.toString(sheetNo))); excelUtil.setColumnTitle(sheet); } //获取数据 vo = exportDataAdapter.getData(); //往excel添加一行数据 excelUtil.addRowData(vo, wb, sheet, rowNum); rowNum++; //准备生成下一个sheetName if (rowNum == SHEET_SIZE + 1) { rowNum = 1; } } //生产者不再生产数据，取剩余数据，并将数据写入excel Integer reminderDataSize = exportDataAdapter.getDataSize(); T reminderData; if (reminderDataSize > 0) { for (int i = 0; i < reminderDataSize; i++) { reminderData = exportDataAdapter.getData(); if (rowNum == 1) { sheetNo++; sheet = excelUtil.createSheet(wb, sheetName.concat(Integer.toString(sheetNo))); excelUtil.setColumnTitle(sheet); } excelUtil.addRowData(reminderData, wb, sheet, rowNum); rowNum++; if (rowNum == SHEET_SIZE + 1) { rowNum = 1; } } } log.info("数据导出完成"); latch.countDown(); } }ExcelUtil/** * @author Johnny * @Date: 2020/4/28 21:47 * @Description: */ public class ExcelUtil<T> { Class<T> clazz; /** * 表头字段列表 */ private List<Field> fields; /** * 数字单元格对象 */ private CellStyle decimalCellStyle = null; /** * 日期时间单元格对象 */ private CellStyle dateTimeCellStyle = null; /** * 构造方法 * * @param clazz */ public ExcelUtil(Class<T> clazz) { this.clazz = clazz; } /** * 添加一条数据 * * @param vo:需要导出的vo对象 * @param wb：工作簿 * @param sheet：工作表 * @param rowNum：当前行号 */ public void addRowData(T vo, SXSSFWorkbook wb, Sheet sheet, int rowNum) { //创建一行 Row row = sheet.createRow(rowNum); Field field; Cell cell; ExcelVoAttribute attr; int fieldSize = fields.size(); // 遍历入参对象的所有属性 for (int j = 0; j < fieldSize; j++) { // 通过反射获得需要导出的入参对象的所有属性 field = fields.get(j); // 设置实体类私有属性可访问 field.setAccessible(true); // 获取所有添加了注解的属性 attr = field.getAnnotation(ExcelVoAttribute.class); // 给每个属性创建一个单元格 cell = row.createCell(j); try { this.setCellValue(attr, field.get(vo), wb, cell); } catch (IllegalAccessException e) { e.printStackTrace(); } } } /** * 根据注解类判断字段类型设置excel单元格数据格式方法 * * @param attr * @param valueObject * @param workbook * @param cell */ private void setCellValue(ExcelVoAttribute attr, Object valueObject, SXSSFWorkbook workbook, Cell cell) { String returnValue; if (attr.isNumber()) { cell.setCellStyle(getCellStyle(attr, workbook)); returnValue = valueObject == null || "".equals(valueObject) ? "0" : valueObject.toString(); BigDecimal num = new BigDecimal(returnValue); cell.setCellValue(num.doubleValue()); } else if (attr.isDateTime()) { cell.setCellStyle(getCellStyle(attr, workbook)); DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); returnValue = df.format((TemporalAccessor) valueObject); cell.setCellValue(returnValue); } else { returnValue = valueObject == null ? "" : valueObject.toString(); cell.setCellValue(returnValue); } } /** * 根据注解类判断字段类型，返回excel单元格数据格式方法 * * @param attr * @param workbook * @return CellStyle */ private CellStyle getCellStyle(ExcelVoAttribute attr, SXSSFWorkbook workbook) { if (attr.isNumber()) { if (decimalCellStyle == null) { decimalCellStyle = workbook.createCellStyle(); //此处设置数字单元格格式 DataFormat df = workbook.createDataFormat(); //千分位,保留1位小数 decimalCellStyle.setDataFormat(df.getFormat("#,##0.0")); } return decimalCellStyle; } if (attr.isDateTime()) { if (dateTimeCellStyle == null) { dateTimeCellStyle = workbook.createCellStyle(); //此处设置日期时间单元格格式 DataFormat df = workbook.createDataFormat(); dateTimeCellStyle.setDataFormat(df.getFormat("yyyy-MM-dd HH:mm:ss")); } return dateTimeCellStyle; } return null; } /** * 创建工作页Sheet * * @param wb * @param sheetName * @return Sheet */ public Sheet createSheet(SXSSFWorkbook wb, String sheetName) { return wb.createSheet(sheetName); } /** * 设置excel列头及格式 * * @param sheet */ public void setColumnTitle(Sheet sheet) { if (fields == null) { this.fields = this.getSortFields(); } Row row; Cell cell; ExcelVoAttribute attr; Field field; int fieldSize = fields.size(); row = sheet.createRow(0); for (int i = 0; i < fieldSize; i++) { field = fields.get(i); attr = field.getAnnotation(ExcelVoAttribute.class); cell = CellUtil.createCell(row, i, attr.name()); // 设置列宽，根据相应的字段名的长度等比 sheet.setColumnWidth(i, attr.name().getBytes().length * 400); } } /** * 获取输出对象字段列表，并根据注解进行字段排序 * * @return */ private List<Field> getSortFields() { List<Field> fields = Arrays.stream(clazz.getDeclaredFields()).filter(x -> x.isAnnotationPresent(ExcelVoAttribute.class)).collect(Collectors.toList()); List<Field> sortList = new ArrayList<>(fields); //排序 for (Field field : fields) { ExcelVoAttribute excelVoAttribute = field.getAnnotation(ExcelVoAttribute.class); int sortNo = excelVoAttribute.column(); sortList.set(sortNo, field); } return sortList; } }ExcelUtils/** * @author Johnny * @Date: 2020/4/25 22:56 * @Description: */ @Slf4j public class ExcelUtils { /** * 导出歌曲 * @param response * @param list * @param map * @param title */ public static void exportExcel(HttpServletResponse response, List list, Map<String, String> map, String title) { //通过工具类创建writer ExcelWriter writer = ExcelUtil.getWriter(true); //自定义标题别名 Set<Map.Entry<String, String>> entries = map.entrySet(); //迭代器遍历数据 Iterator<Map.Entry<String, String>> iterator = entries.iterator(); while (iterator.hasNext()) { Map.Entry<String, String> next = iterator.next(); //自定义表头 writer.addHeaderAlias(next.getKey(), next.getValue()); } //合并单元格后的标题行，使用默认标题样式 writer.merge(map.size() - 1, title); //一次性写出内容，使用默认样式，强制输出标题 writer.write(list, true); //out为outputStream，需要写出到的目标流 try { writer.flush(response.getOutputStream(), true); } catch (IOException e) { log.info("歌单导出异常"); e.printStackTrace(); } writer.close(); } /** * 导入歌曲 * @param file * @return */ public static List<Song> importExcel(File file) { List<Song> songs = new ArrayList<>(); InputStream inputStream = null; try { inputStream = new FileInputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); } //通过getReader方法确定读取那个sheet中的数据 ExcelReader reader = ExcelUtil.getReader(inputStream, "sheet1"); //回去总行数 List<List<Object>> read = reader.read(1, reader.getRowCount()); for (List<Object> objects : read) { //对每行数据取出构建一个song对象 Song song = Song.builder() .songName(objects.get(0).toString()) .songId(UUID.randomUUID().toString().replace("-", "")) .sortId("0") .singer(objects.get(1).toString()) .duration(objects.get(2).toString()) .thumbnail(objects.get(3).toString()) .url(objects.get(4).toString()) .lyric(objects.get(5).toString()) .commentCount(0) .playCount(0) .deleteFlag("0") .updateTime(LocalDateTime.now()) .createTime(LocalDateTime.now()) .build(); songs.add(song); } return songs; } }ExportDataAdapter/** * @author Johnny * @Date: 2020/4/28 21:53 * @Description: */ public class ExportDataAdapter<T>{ /** * 默认队列大小 */ private static Integer DEFAULT_SIZE = 1000; private BlockingQueue<T> resourceQueue = null; public ExportDataAdapter() { this.resourceQueue = new LinkedBlockingQueue<T>(DEFAULT_SIZE); } /** * 添加数据 * * @param data */ public void addData(T data) { try { resourceQueue.put(data); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 获取剩余数据数量 * * @return */ public Integer getDataSize() { return resourceQueue.size(); } /** * 从队列中获取数据 * * @return */ public T getData() { try { return resourceQueue.take(); } catch (InterruptedException e) { e.printStackTrace(); } return null; } }FileUtil/** * @author Johnny * @Date: 2020/4/26 10:41 * @Description: */ public class FileUtil { public static File fileConversion(MultipartFile file) { int n; File file1 = new File(file.getOriginalFilename()); try { InputStream in = file.getInputStream(); OutputStream os = new FileOutputStream(file1); byte[] bytes = new byte[4096]; while ((n=in.read(bytes,0,4096))!=-1){ os.write(bytes,0,n); File file2 = new File(file1.toURI()); file2.delete(); } } catch (IOException e) { e.printStackTrace(); } return file1; } }JwtTokenUtil/** * @Author Johnny * @Date 2020/4/15 * @Version 1.0 */ @Slf4j public class JwtTokenUtil { /** * 加密 * * @param userId * @param expiresAt * @return String */ public static String getToken(final String userId, String userRole,final String secrect,Date expiresAt) { String token = null; try { token = JWT.create() .withIssuer("auth0") .withClaim("userId", userId) .withClaim("userRole", userRole) .withExpiresAt(expiresAt) // 使用了HMAC256加密算法, mySecret是用来加密数字签名的密钥 .sign(Algorithm.HMAC256(secrect)); } catch (UnsupportedEncodingException e) { log.error("不支持的编码格式"); } return token; } /** * 解密 * @param token * @return DecodedJWT */ public static DecodedJWT deToken(final String token,final String secrect) { DecodedJWT jwt; JWTVerifier verifier = null; try { verifier = JWT.require(Algorithm.HMAC256(secrect)) .withIssuer("auth0") .build(); } catch (UnsupportedEncodingException e) { log.error("不支持的编码格式"); } assert verifier != null; jwt = verifier.verify(token); return jwt; } /** * 获取userId * * @param token * @return String */ public static String getUserId(String token,String secrect) { return deToken(token,secrect).getClaim("userId").asString(); } /** * 获取role * * @param token * @return String */ public static String getRoles(String token,String secrect) { return deToken(token,secrect).getClaim("userRole").asString(); } /** * 验证是否过期 * * @param token * @return boolean */ public static boolean isExpiration(String token,String secrect) { return deToken(token,secrect).getExpiresAt().before(new Date()); } public static void main(String[] args) { // String token = getToken("2000100193", new Date(System.currentTimeMillis() + 10L * 1000L)); // System.out.println(token); // while (true) { // boolean flag = isExpiration(token); // System.out.println(flag); // if (flag) { // System.out.println("token已失效"); // break; // } // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // } // String token = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhdXRoMCIsImV4cCI6MTU4NzY3MTQ3OCwidXNlcklkIjoiREUzNUQ3Q0MwNUFGOTZBMjFEN0FERkM4NjUxRTY2MTQifQ.uT2W3QcE3744WNN3inEKT8lUVJs6xAC7TodDCaWkcyM"; // System.out.println(deToken(token).getClaim("userId").asString()); } }Md5Util/** * @author Johnny */ public class Md5Util { /** * @param pwd 需要加密的字符串 * @param isUpper 字母大小写(false为默认小写，true为大写) * @param bit 加密的位数（16,32,64） * @return String */ public static String getMd5(String pwd, boolean isUpper, Integer bit) { String md5 = ""; try { // 创建加密对象 MessageDigest md = MessageDigest.getInstance("md5"); if (bit == 64) { Base64.Encoder encoder = Base64.getEncoder(); md5 = encoder.encodeToString(md.digest(pwd.getBytes(StandardCharsets.UTF_8))); } else { // 计算MD5函数 md.update(pwd.getBytes()); byte b[] = md.digest(); int i; StringBuilder sb = new StringBuilder(); for (byte value : b) { i = value; if (i < 0) { i += 256; } if (i < 16) { sb.append("0"); } sb.append(Integer.toHexString(i)); } md5 = sb.toString(); if (bit == 16) { //截取32位md5为16位 md5 = md5.substring(8, 24).toString(); if (isUpper) { md5 = md5.toUpperCase(); } return md5; } } //转换成大写 if (isUpper) { md5 = md5.toUpperCase(); } } catch (Exception e) { e.printStackTrace(); System.out.println("md5加密抛出异常！"); } return md5; } public static void main(String[] args) { String a = "123456"; String md5a = getMd5(a, true, 32); System.out.println(md5a); System.out.println(md5a.length()); }ThreadPool/** * @author Johnny * @Date: 2020/4/28 21:55 * @Description: */ public class ThreadPool { /** * 异步线程 */ private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 100, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(2000), r -> new Thread(r, "excelExportThread"), new ThreadPoolExecutor.AbortPolicy()); public static ThreadPoolExecutor getExecutor() { return executor; } }TreeBuilder/** * @author Johnny * @Date: 2020/4/23 18:46 * @Description: */ public class TreeBuilder { /** * 两层循环实现建树 * * @param treeNodes 传入的树节点列表 * @return */ public static List<TreeNode> buildTreeByLoop(List<TreeNode> treeNodes) { List<TreeNode> trees = new ArrayList<>(); for (TreeNode treeNode : treeNodes) { if (treeNode.getParentId() == 0) { trees.add(treeNode); } for (TreeNode it : treeNodes) { if (it.getParentId().equals(treeNode.getId())) { if (treeNode.getSubMenus() == null) { treeNode.setSubMenus(new ArrayList<>()); } treeNode.getSubMenus().add(it); } } } return trees; } /** * 使用递归方法建树 * * @param treeNodes * @return */ public static List<TreeNode> buildTreeByRecursive(List<TreeNode> treeNodes) { List<TreeNode> trees = new ArrayList<>(); for (TreeNode treeNode : treeNodes) { if (treeNode.getParentId() == 0) { trees.add(findChildren(treeNode, treeNodes)); } } return trees; } /** * 递归查找子节点 * * @param treeNodes * @return */ public static TreeNode findChildren(TreeNode treeNode, List<TreeNode> treeNodes) { for (TreeNode it : treeNodes) { if (treeNode.getId().equals(it.getParentId())) { if (treeNode.getSubMenus() == null) { treeNode.setSubMenus(new ArrayList<>()); } treeNode.getSubMenus().add(findChildren(it, treeNodes)); } } return treeNode; } }TreeNode/** * @author Johnny * @Date: 2020/4/23 18:46 * @Description: */ @Data @NoArgsConstructor @AllArgsConstructor @Builder public class TreeNode { private Integer id; private Integer parentId; private Integer type; private String title; private String icon; private String path; private Integer sort; private List<TreeNode> subMenus; public TreeNode(Integer id, Integer parentId, Integer type,String title, String icon, String path, Integer sort) { this.id = id; this.parentId = parentId; this.type = type; this.title = title; this.icon = icon; this.path = path; this.sort = sort; } }
@Controller处理由DispatcherServlet 分发的请求，用于类上把用户请求的数据经过业务处理层处理之后封装成一个Model再把该Model 返回给对应的View 进行展示@RequestMapping处理请求地址映射的注解，可用于类或方法属性value： 指定请求的实际地址，指定的地址可以是URI Template 模式method： 指定请求的method类型， GET、POST、PUT、DELETE...consumes： 指定处理请求的提交内容类型（Content-Type），例如application/json, text/htmlproduces: 指定返回的内容类型，仅当request请求头中的(Accept)类型中包含该指定类型才返回params： 指定request中必须包含某些参数值是，才让该方法处理headers： 指定request中必须包含某些指定的header值，才能让该方法处理请求@Resource、@Autowired在bean的注入时使用写在字段和setter方法上。两者如果都写在字段上，那么就不需要再写setter方法@PathVariable用于将请求URL中的模板变量映射到功能处理方法的参数上，即取出uri模板中的变量作为参数@requestParam主要用于在SpringMVC后台控制层获取参数，类似一种是request.getParameter("name")@ResponseBody将Controller的方法返回的对象，通过适当的HttpMessageConverter转换为指定格式@Component通用注解（可代替@Repository @Service @Controller）@Repository用于注解dao层，在daoImpl类上面注解@ModelAttribute、@SessionAttributes传递和保存数据，在不同的模型（model）和控制器之间共享数据@PathVariable、@RequestParam@PathVariable获取请求路径上的变量值@RequestParam用来获得静态的URL请求入参 spring注解时action里用到@RequestHeader、@CookieValue@RequestHeader可以把Request请求header部分的值绑定到方法的参数上。@CookieValue可以把Request header中关于cookie的值绑定到方法的参数上@RequestParam, @RequestBody@RequestParam用来处理Content-Type: 为 application/x-www-form-urlencoded编码的内容，提交方式GET、POST@RequestBody常用来处理Content-Type: 不是application/x-www-form-urlencoded编码的内容，例如application/json,application/xml等