Repository: fenixsoft/monolithic_arch_springboot Branch: master Commit: bcd4e69cb201 Files: 102 Total size: 2.1 MB Directory structure: gitextract_udx1wp_1/ ├── .gitignore ├── .mvn/ │ └── wrapper/ │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── github/ │ │ │ └── fenixsoft/ │ │ │ └── bookstore/ │ │ │ ├── BookstoreApplication.java │ │ │ ├── applicaiton/ │ │ │ │ ├── AccountApplicationService.java │ │ │ │ ├── ProductApplicationService.java │ │ │ │ └── payment/ │ │ │ │ ├── PaymentApplicationService.java │ │ │ │ └── dto/ │ │ │ │ └── Settlement.java │ │ │ ├── domain/ │ │ │ │ ├── BaseEntity.java │ │ │ │ ├── account/ │ │ │ │ │ ├── Account.java │ │ │ │ │ ├── AccountRepository.java │ │ │ │ │ └── validation/ │ │ │ │ │ ├── AccountValidation.java │ │ │ │ │ ├── AuthenticatedAccount.java │ │ │ │ │ ├── ExistsAccount.java │ │ │ │ │ ├── NotConflictAccount.java │ │ │ │ │ └── UniqueAccount.java │ │ │ │ ├── auth/ │ │ │ │ │ ├── AuthenticAccount.java │ │ │ │ │ ├── AuthenticAccountRepository.java │ │ │ │ │ ├── Role.java │ │ │ │ │ ├── provider/ │ │ │ │ │ │ ├── PreAuthenticatedAuthenticationProvider.java │ │ │ │ │ │ └── UsernamePasswordAuthenticationProvider.java │ │ │ │ │ └── service/ │ │ │ │ │ ├── AuthenticAccountDetailsService.java │ │ │ │ │ ├── JWTAccessToken.java │ │ │ │ │ ├── JWTAccessTokenService.java │ │ │ │ │ └── OAuthClientDetailsService.java │ │ │ │ ├── payment/ │ │ │ │ │ ├── Payment.java │ │ │ │ │ ├── PaymentRepository.java │ │ │ │ │ ├── PaymentService.java │ │ │ │ │ ├── Stockpile.java │ │ │ │ │ ├── StockpileRepository.java │ │ │ │ │ ├── StockpileService.java │ │ │ │ │ ├── Wallet.java │ │ │ │ │ ├── WalletRepository.java │ │ │ │ │ ├── WalletService.java │ │ │ │ │ └── validation/ │ │ │ │ │ ├── SettlementValidator.java │ │ │ │ │ └── SufficientStock.java │ │ │ │ └── warehouse/ │ │ │ │ ├── Advertisement.java │ │ │ │ ├── AdvertisementRepository.java │ │ │ │ ├── Product.java │ │ │ │ ├── ProductRepository.java │ │ │ │ ├── ProductService.java │ │ │ │ └── Specification.java │ │ │ ├── infrastructure/ │ │ │ │ ├── cache/ │ │ │ │ │ └── CacheConfiguration.java │ │ │ │ ├── configuration/ │ │ │ │ │ ├── AuthenticationServerConfiguration.java │ │ │ │ │ ├── AuthorizationServerConfiguration.java │ │ │ │ │ ├── JerseyConfiguration.java │ │ │ │ │ ├── ResourceServerConfiguration.java │ │ │ │ │ └── WebSecurityConfiguration.java │ │ │ │ ├── jaxrs/ │ │ │ │ │ ├── AccessDeniedExceptionMapper.java │ │ │ │ │ ├── BaseExceptionMapper.java │ │ │ │ │ ├── CodedMessage.java │ │ │ │ │ ├── CommonResponse.java │ │ │ │ │ └── ViolationExceptionMapper.java │ │ │ │ └── utility/ │ │ │ │ └── Encryption.java │ │ │ └── resource/ │ │ │ ├── AccountResource.java │ │ │ ├── AdvertisementResource.java │ │ │ ├── PaymentResource.java │ │ │ ├── ProductResource.java │ │ │ └── SettlementResource.java │ │ └── resources/ │ │ ├── application-mysql.yml │ │ ├── application-test.yml │ │ ├── application.yml │ │ ├── banner.txt │ │ ├── db/ │ │ │ ├── hsqldb/ │ │ │ │ ├── data.sql │ │ │ │ └── schema.sql │ │ │ └── mysql/ │ │ │ ├── data.sql │ │ │ ├── schema.sql │ │ │ └── user.sql │ │ └── static/ │ │ ├── index.html │ │ └── static/ │ │ ├── board/ │ │ │ ├── gitalk.css │ │ │ ├── gitalk.html │ │ │ └── gitalk.min.js │ │ ├── css/ │ │ │ └── app.13440f960e43a3574b009b7352447f18.css │ │ └── js/ │ │ ├── 0.c178f427b3d08777c70f.js │ │ ├── 1.a33faf036923758c7965.js │ │ ├── 2.626ed94f3752555e21f0.js │ │ ├── 3.bc7f0b2154007257c317.js │ │ ├── 4.b4e48a42cf742af20851.js │ │ ├── 5.d375cbd6c7e1463cdbed.js │ │ ├── 6.68562501db5734ef1531.js │ │ ├── 7.184a5e39cc0c624f6a6d.js │ │ ├── 8.176f9455c3442c06ebf6.js │ │ ├── 9.527be297aba1594ffe0d.js │ │ ├── app.ea66dc0be78c3ed2ae63.js │ │ ├── manifest.0437a7f02d3154ee1abb.js │ │ └── vendor.c2f13a2146485051ae24.js │ └── test/ │ └── java/ │ └── com/ │ └── github/ │ └── fenixsoft/ │ └── bookstore/ │ ├── DBRollbackBase.java │ └── resource/ │ ├── AccountResourceTest.java │ ├── AdvertisementResourceTest.java │ ├── AuthResourceTest.java │ ├── JAXRSResourceBase.java │ ├── PaymentResourceTest.java │ └── ProductResourceTest.java └── travis_docker_push.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/** !**/src/test/** ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ ### VS Code ### .vscode/ ================================================ FILE: .mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if (mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if (mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if (!outputFile.getParentFile().exists()) { if (!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: .travis.yml ================================================ language: java jdk: - openjdk12 before_install: - export TZ='Asia/Shanghai' install: - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V script: - mvn clean test package jacoco:report coveralls:report -DrepoToken=$repoToken deploy: - provider: script keep_history: true skip_cleanup: true script: bash travis_docker_push.sh on: branch: master ================================================ FILE: Dockerfile ================================================ FROM openjdk:12-alpine MAINTAINER icyfenix ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \ JAVA_OPTS="" \ PROFILES="default" ADD /target/*.jar /bookstore.jar ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /bookstore.jar --spring.profiles.active=$PROFILES"] EXPOSE 8080 ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Fenix's BookStore后端:以单体架构实现

logo

Travis-CI Coverage Status License Document License About Author

如果你此时并不曾了解过什么是“The Fenix Project”,建议先阅读这部分内容。 单体架构是Fenix's Bookstore'第一个版本的服务端实现,它与此后基于微服务(Spring Cloud、Kubernetes)、服务网格(Istio)、无服务(Serverless)架构风格实现的其他版本,在业务功能上的表现是完全一致的。如果你不是针对性地带着解决某个具体问题、了解某项具体工具、技术的目的而来,而是时间充裕,希望了解软件架构的全貌与发展的话,笔者推荐以此工程入手来了解现代软件架构,因为单体架构的结构是相对直观的,易于理解的架构,对后面接触的其他架构风格也起良好的铺垫作用。此外,笔者在对应的文档中详细分析了作为一个架构设计者,会考虑哪些的通用问题,希望把抽象的“架构”一词具象化出来。 ## 运行程序 以下几种途径,可以运行程序,浏览最终的效果: - 通过Docker容器方式运行: > ```bash > $ docker run -d -p 8080:8080 --name bookstore icyfenix/bookstore:monolithic > ``` > > 然后在浏览器访问:[http://localhost:8080](http://localhost:8080),系统预置了一个用户(user:icyfenix,pw:123456),也可以注册新用户来测试。 > > 默认会使用HSQLDB的内存模式作为数据库,并在系统启动时自动初始化好了Schema,完全开箱即用。但这同时也意味着当程序运行结束时,所有的数据都将不会被保留。 > > 如果希望使用HSQLDB的文件模式,或者其他非嵌入式的独立的数据库支持的话,也是很简单的。以常用的MySQL/MariaDB为例,程序中也已内置了MySQL的表结构初始化脚本,你可以使用环境变量“PROFILES”来激活SpringBoot中针对MySQL所提供的配置,命令如下所示: > > ```bash > $ docker run -d -p 8080:8080 --name bookstore icyfenix/bookstore:monolithic -e PROFILES=mysql > ``` > > 此时你需要通过Docker link、Docker Compose或者直接在主机的Host文件中提供一个名为“mysql_lan”的DNS映射,使程序能顺利链接到数据库,关于数据库的更多配置,可参考源码中的[application-mysql.yml](https://github.com/fenixsoft/monolithic_arch_springboot/blob/70f435911b0e0753d7e4cee27cd96304dbef786d/src/main/resources/application-mysql.yml)。 - 通过Git上的源码,以Maven运行: >```bash ># 克隆获取源码 >$ git clone https://github.com/fenixsoft/monolithic_arch_springboot.git > ># 进入工程根目录 >$ cd monolithic_arch_springboot > ># 编译打包 ># 采用Maven Wrapper,此方式只需要机器安装有JDK 8或以上版本即可,无需包括Maven在内的其他任何依赖 ># 如在Windows下应使用mvnw.cmd package代替以下命令 >$ ./mvnw package > ># 运行程序,地址为localhost:8080 >$ java -jar target/bookstore-1.0.0-Monolithic-SNAPSHOT.jar >``` > >然后在浏览器访问:[http://localhost:8080](http://localhost:8080),系统预置了一个用户(user:icyfenix,pw:123456),也可以注册新用户来测试。 - 通过Git上的源码,在IDE环境中运行: > - 以IntelliJ IDEA为例,Git克隆本项目后,在File -> Open菜单选择本项目所在的目录,或者pom.xml文件,以Maven方式导入工程。 > > - IDEA将自动识别出这是一个SpringBoot工程,并定位启动入口为BookstoreApplication,待IDEA内置的Maven自动下载完所有的依赖包后,运行该类即可启动。 > > - 如你使用其他的IDE,没有对SpringBoot的直接支持,亦可自行定位到BookstoreApplication,这是一个带有main()方法的Java类,运行即可。 > > - 可通过IDEA的Maven面板中Lifecycle里面的package来对项目进行打包、发布。 > > - 在IDE环境中修改配置(如数据库等)会更加简单,具体可以参考工程中application.yml和application-mysql.yml中的内容。 ## 技术组件 Fenix's BookStore单体架构后端尽可能采用标准的技术组件进行构建,不依赖与具体的实现,包括: - [JSR 370:Java API for RESTful Web Services 2.1](https://jcp.org/en/jsr/detail?id=370)(JAX-RS 2.1)
RESTFul服务方面,采用的实现为Jersey 2,亦可替换为Apache CXF、RESTeasy、WebSphere、WebLogic等 - [JSR 330:Dependency Injection for Java 1.0](https://jcp.org/en/jsr/detail?id=330)
依赖注入方面,采用的的实现为SpringBoot 2中内置的Spring Framework 5。虽然在多数场合中尽可能地使用了JSR 330的标准注解,但仍有少量地方由于Spring在对@Named、@Inject等注解的支持表现上与本身提供的注解差异,使用了Spring的私有注解。如替换成其他的CDI实现,如HK2,需要较大的改动 - [JSR 338:Java Persistence 2.2](https://jcp.org/en/jsr/detail?id=338)
持久化方面,采用的实现为Spring Data JPA。可替换为Batoo JPA、EclipseLink、OpenJPA等实现,只需将使用CrudRepository所省略的代码手动补全回来即可,无需其他改动。 - [JSR 380:Bean Validation 2.0](https://jcp.org/en/jsr/detail?id=380)
数据验证方面,采用的实现为Hibernate Validator 6,可替换为Apache BVal等其他验证框架 - [JSR 315:Java Servlet 3.0](https://jcp.org/en/jsr/detail?id=315)
Web访问方面,采用的实现为SpringBoot 2中默认的Tomcat 9 Embed,可替换为Jetty、Undertow等其他Web服务器 有以下组件仍然依赖了非标准化的技术实现,包括: - [JSR 375:Java EE Security API specification 1.0](https://jcp.org/en/jsr/detail?id=375)
认证/授权方面,在2017年才发布的JSR 375中仍然没有直接包含OAuth2和JWT的直接支持,因后续实现微服务架构时对比的需要,单体架构中选择了Spring Security 5作为认证服务,Spring Security OAuth 2.3作为授权服务,Spring Security JWT作为JWT令牌支持,并未采用标准的JSR 375实现,如Soteria。 - [JSR 353/367:Java API for JSON Processing/Binding](https://jcp.org/en/jsr/detail?id=353)
在JSON序列化/反序列化方面,由于Spring Security OAuth的限制(使用JSON-B作为反序列化器时的结果与Jackson等有差异),采用了Spring Security OAuth默认的Jackson,并未采用标准的JSR 353/367实现,如Apache Johnzon、Eclipse Yasson等。 ## 工程结构 Fenix's BookStore单体架构后端参考(并未完全遵循)了DDD的分层模式和设计原则,整体分为以下四层: 1. Resource:对应DDD中的User Interface层,负责向用户显示信息或者解释用户发出的命令。请注意,这里指的“用户”不一定是使用用户界面的人,可以是位于另一个进程或计算机的服务。由于本工程采用了MVVM前后端分离模式,这里所指的用户实际上是前端的服务消费者,所以这里以RESTFul中的核心概念”资源“(Resource)来命名。 2. Application:对应DDD中的Application层,负责定义软件本身对外暴露的能力,即软件本身可以完成哪些任务,并负责对内协调领域对象来解决问题。根据DDD的原则,应用层要尽量简单,不包含任何业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作,这一点在代码上表现为Application层中一般不会存在任何的条件判断语句。在许多项目中,Application层都会被选为包裹事务(代码进入此层事务开始,退出此层事务提交或者回滚)的载体。 3. Domain:对应DDD中的Domain层,负责实现业务逻辑,即表达业务概念,处理业务状态信息以及业务规则这些行为,此层是整个项目的重点。 4. Infrastructure:对应DDD中的Infrastructure层,向其他层提供通用的技术能力,譬如持久化能力、远程服务通讯、工具集,等等。

## 协议 - 本文档代码部分采用[Apache 2.0协议](https://www.apache.org/licenses/LICENSE-2.0)进行许可。遵循许可的前提下,你可以自由地对代码进行修改,再发布,可以将代码用作商业用途。但要求你: - **署名**:在原有代码和衍生代码中,保留原作者署名及代码来源信息。 - **保留许可证**:在原有代码和衍生代码中,保留Apache 2.0协议文件。 - 本作品文档部分采用[知识共享署名 4.0 国际许可协议](http://creativecommons.org/licenses/by/4.0/)进行许可。 遵循许可的前提下,你可以自由地共享,包括在任何媒介上以任何形式复制、发行本作品,亦可以自由地演绎、修改、转换或以本作品为基础进行二次创作。但要求你: - **署名**:应在使用本文档的全部或部分内容时候,注明原作者及来源信息。 - **非商业性使用**:不得用于商业出版或其他任何带有商业性质的行为。如需商业使用,请联系作者。 - **相同方式共享的条件**:在本文档基础上演绎、修改的作品,应当继续以知识共享署名 4.0国际许可协议进行许可。 ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.5.RELEASE com.github.fenixsoft bookstore-monolithic-springboot 1.0.0-SNAPSHOT bookstore-monolithic-springboot Monolithic Architecture Demonstrate with Spring Boot 1.8 -Dfile.encoding=UTF-8 org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-jersey org.springframework.boot spring-boot-starter-security org.springframework.security.oauth spring-security-oauth2 2.3.5.RELEASE org.springframework.security spring-security-jwt 1.0.10.RELEASE org.springframework.boot spring-boot-starter-cache com.github.ben-manes.caffeine caffeine 2.6.2 org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.security spring-security-test test org.hsqldb hsqldb runtime mysql mysql-connector-java runtime org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin 8 8 org.apache.maven.plugins maven-surefire-plugin 2.22.2 false org.eluder.coveralls coveralls-maven-plugin 4.3.0 javax.xml.bind jaxb-api 2.2.3 org.jacoco jacoco-maven-plugin 0.8.5 prepare-agent prepare-agent ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/BookstoreApplication.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; @SpringBootApplication @EnableCaching @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true) public class BookstoreApplication { public static void main(String[] args) { SpringApplication.run(BookstoreApplication.class, args); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/applicaiton/AccountApplicationService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.applicaiton; import com.github.fenixsoft.bookstore.domain.account.Account; import com.github.fenixsoft.bookstore.domain.account.AccountRepository; import com.github.fenixsoft.bookstore.infrastructure.utility.Encryption; import javax.inject.Inject; import javax.inject.Named; import javax.transaction.Transactional; /** * 用户资源的应用服务接口 * * @author icyfenix@gmail.com * @date 2020/3/10 17:46 **/ @Named @Transactional public class AccountApplicationService { @Inject private AccountRepository repository; @Inject private Encryption encoder; public void createAccount(Account account) { account.setPassword(encoder.encode(account.getPassword())); repository.save(account); } public Account findAccountByUsername(String username) { return repository.findByUsername(username); } public void updateAccount(Account account) { repository.save(account); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/applicaiton/ProductApplicationService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.applicaiton; import com.github.fenixsoft.bookstore.domain.payment.Stockpile; import com.github.fenixsoft.bookstore.domain.payment.StockpileService; import com.github.fenixsoft.bookstore.domain.warehouse.Product; import com.github.fenixsoft.bookstore.domain.warehouse.ProductService; import javax.inject.Inject; import javax.inject.Named; import javax.transaction.Transactional; /** * 产品的应用服务接口 * * @author icyfenix@gmail.com * @date 2020/3/15 20:05 **/ @Named @Transactional public class ProductApplicationService { @Inject private ProductService service; @Inject private StockpileService stockpileService; /** * 获取仓库中所有的货物信息 */ public Iterable getAllProducts() { return service.getAllProducts(); } /** * 获取仓库中指定的货物信息 */ public Product getProduct(Integer id) { return service.getProduct(id); } /** * 创建或更新产品信息 */ public Product saveProduct(Product product) { return service.saveProduct(product); } /** * 删除指定产品 */ public void removeProduct(Integer id) { service.removeProduct(id); } /** * 根据产品查询库存 */ public Stockpile getStockpile(Integer productId) { return stockpileService.getByProductId(productId); } /** * 将指定的产品库存调整为指定数额 */ public void setStockpileAmountByProductId(Integer productId, Integer amount) { stockpileService.set(productId, amount); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/applicaiton/payment/PaymentApplicationService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.applicaiton.payment; import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; import com.github.fenixsoft.bookstore.domain.payment.*; import com.github.fenixsoft.bookstore.domain.warehouse.ProductService; import org.springframework.cache.Cache; import javax.annotation.Resource; import javax.inject.Inject; import javax.inject.Named; import javax.transaction.Transactional; /** * 支付应用务 * * @author icyfenix@gmail.com * @date 2020/3/12 16:29 **/ @Named @Transactional public class PaymentApplicationService { @Inject private PaymentService paymentService; @Inject private ProductService productService; @Inject private WalletService walletService; @Resource(name = "settlement") private Cache settlementCache; /** * 根据结算清单的内容执行,生成对应的支付单 */ public Payment executeBySettlement(Settlement bill) { // 从服务中获取商品的价格,计算要支付的总价(安全原因,这个不能由客户端传上来) productService.replenishProductInformation(bill); // 冻结部分库存(保证有货提供),生成付款单 Payment payment = paymentService.producePayment(bill); // 设立解冻定时器(超时未支付则释放冻结的库存和资金) paymentService.setupAutoThawedTrigger(payment); return payment; } /** * 完成支付 * 立即取消解冻定时器,执行扣减库存和资金 */ public void accomplishPayment(Integer accountId, String payId) { // 订单从冻结状态变为派送状态,扣减库存 double price = paymentService.accomplish(payId); // 扣减货款 walletService.decrease(accountId, price); // 支付成功的清除缓存 settlementCache.evict(payId); } /** * 取消支付 * 立即触发解冻定时器,释放库存和资金 */ public void cancelPayment(String payId) { // 释放冻结的库存 paymentService.cancel(payId); // 支付成功的清除缓存 settlementCache.evict(payId); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/applicaiton/payment/dto/Settlement.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.applicaiton.payment.dto; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.fenixsoft.bookstore.domain.warehouse.Product; import javax.validation.constraints.Min; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.util.Collection; import java.util.Map; /** * 支付结算单模型 * * @author icyfenix@gmail.com * @date 2020/3/12 11:35 **/ public class Settlement { @Size(min = 1, message = "结算单中缺少商品清单") private Collection items; @NotNull(message = "结算单中缺少配送信息") private Purchase purchase; /** * 购物清单中的商品信息 * 基于安全原因(避免篡改价格),改信息不会取客户端的,需在服务端根据商品ID再查询出来 */ public transient Map productMap; public Collection getItems() { return items; } public void setItems(Collection items) { this.items = items; } public Purchase getPurchase() { return purchase; } public void setPurchase(Purchase purchase) { this.purchase = purchase; } /** * 结算单中要购买的商品 */ public static class Item { @NotNull(message = "结算单中必须有明确的商品数量") @Min(value = 1, message = "结算单中商品数量至少为一件") private Integer amount; @JsonProperty("id") @NotNull(message = "结算单中必须有明确的商品信息") private Integer productId; public Integer getAmount() { return amount; } public void setAmount(Integer amount) { this.amount = amount; } public Integer getProductId() { return productId; } public void setProductId(Integer productId) { this.productId = productId; } } /** * 结算单中的配送信息 */ public static class Purchase { private Boolean delivery = true; @NotEmpty(message = "配送信息中缺少支付方式") private String pay; @NotEmpty(message = "配送信息中缺少收件人姓名") private String name; @NotEmpty(message = "配送信息中缺少收件人电话") private String telephone; @NotEmpty(message = "配送信息中缺少收件地址") private String location; public Boolean getDelivery() { return delivery; } public void setDelivery(Boolean delivery) { this.delivery = delivery; } public String getPay() { return pay; } public void setPay(String pay) { this.pay = pay; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getTelephone() { return telephone; } public void setTelephone(String telephone) { this.telephone = telephone; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/BaseEntity.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.MappedSuperclass; import java.io.Serializable; /** * JavaBean领域对象的共同基类,定义了ID属性和其访问字段 * * @author icyfenix@gmail.com * @date 2020/3/6 10:52 **/ @MappedSuperclass public class BaseEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/account/Account.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.account; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.fenixsoft.bookstore.domain.BaseEntity; import javax.persistence.Column; import javax.persistence.Entity; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; /** * 用户实体 * * @author icyfenix@gmail.com * @date 2020/3/6 23:08 */ @Entity public class Account extends BaseEntity { @NotEmpty(message = "用户不允许为空") private String username; // 密码字段不参与序列化(但反序列化是参与的)、不参与更新(但插入是参与的) // 这意味着密码字段不会在获取对象(很多操作都会关联用户对象)的时候泄漏出去; // 也意味着此时“修改密码”一类的功能无法以用户对象资源的接口来处理(因为更新对象时密码不会被更新),需要单独提供接口去完成 @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @Column(updatable = false) private String password; @NotEmpty(message = "用户姓名不允许为空") private String name; private String avatar; @Pattern(regexp = "1\\d{10}", message = "手机号格式不正确") private String telephone; @Email(message = "邮箱格式不正确") private String email; private String location; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public String getTelephone() { return telephone; } public void setTelephone(String telephone) { this.telephone = telephone; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/account/AccountRepository.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.account; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.data.repository.CrudRepository; import java.util.Collection; import java.util.Optional; /** * 用户对象数据仓库 * * @author icyfenix@gmail.com * @date 2020/3/6 23:10 **/ @CacheConfig(cacheNames = "repository.account") public interface AccountRepository extends CrudRepository { @Override Iterable findAll(); @Cacheable(key = "#username") Account findByUsername(String username); /** * 判断唯一性,用户名、邮箱、电话不允许任何一个重复 */ boolean existsByUsernameOrEmailOrTelephone(String username, String email, String telephone); /** * 判断唯一性,用户名、邮箱、电话不允许任何一个重复 */ Collection findByUsernameOrEmailOrTelephone(String username, String email, String telephone); /** * 判断存在性,用户名存在即为存在 */ @Cacheable(key = "#username") boolean existsByUsername(String username); // 覆盖以下父类中需要处理缓存失效的方法 // 父类取不到CacheConfig的配置信息,所以不能抽象成一个通用的父类接口中完成 @Caching(evict = { @CacheEvict(key = "#entity.id"), @CacheEvict(key = "#entity.username") }) S save(S entity); @CacheEvict Iterable saveAll(Iterable entities); @Cacheable(key = "#id") Optional findById(Integer id); @Cacheable(key = "#id") boolean existsById(Integer id); @CacheEvict(key = "#id") void deleteById(Integer id); @CacheEvict(key = "#entity.id") void delete(Account entity); @CacheEvict(allEntries = true) void deleteAll(Iterable entities); @CacheEvict(allEntries = true) void deleteAll(); } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/AccountValidation.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.account.validation; import com.github.fenixsoft.bookstore.domain.account.Account; import com.github.fenixsoft.bookstore.domain.account.AccountRepository; import com.github.fenixsoft.bookstore.domain.auth.AuthenticAccount; import org.springframework.security.core.context.SecurityContextHolder; import javax.inject.Inject; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.function.Predicate; /** * 用户对象校验器 *

* 如,新增用户时,判断该用户对象是否允许唯一,在修改用户时,判断该用户是否存在 * * @author icyfenix@gmail.com * @date 2020/3/11 14:22 **/ public class AccountValidation implements ConstraintValidator { @Inject protected AccountRepository repository; protected Predicate predicate = c -> true; @Override public boolean isValid(Account value, ConstraintValidatorContext context) { // 在JPA持久化时,默认采用Hibernate实现,插入、更新时都会调用BeanValidationEventListener进行验证 // 而验证行为应该尽可能在外层进行,Resource中已经通过@Vaild注解触发过一次验证,这里会导致重复执行 // 正常途径是使用分组验证避免,但@Vaild不支持分组,@Validated支持,却又是Spring的私有标签 // 另一个途径是设置Hibernate配置文件中的javax.persistence.validation.mode参数为“none”,这个参数在Spring的yml中未提供桥接 // 为了避免涉及到数据库操作的验证重复进行,在这里做增加此空值判断,利用Hibernate验证时验证器不是被Spring创建的特点绕开 return repository == null || predicate.test(value); } public static class ExistsAccountValidator extends AccountValidation { public void initialize(ExistsAccount constraintAnnotation) { predicate = c -> repository.existsById(c.getId()); } } public static class AuthenticatedAccountValidator extends AccountValidation { public void initialize(AuthenticatedAccount constraintAnnotation) { predicate = c -> { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if ("anonymousUser".equals(principal)) { return false; } else { AuthenticAccount loginUser = (AuthenticAccount) principal; return c.getId().equals(loginUser.getId()); } }; } } public static class UniqueAccountValidator extends AccountValidation { public void initialize(UniqueAccount constraintAnnotation) { predicate = c -> !repository.existsByUsernameOrEmailOrTelephone(c.getUsername(), c.getEmail(), c.getTelephone()); } } public static class NotConflictAccountValidator extends AccountValidation { public void initialize(NotConflictAccount constraintAnnotation) { predicate = c -> { Collection collection = repository.findByUsernameOrEmailOrTelephone(c.getUsername(), c.getEmail(), c.getTelephone()); // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突 return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId())); }; } } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/AuthenticatedAccount.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.account.validation; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 代表用户必须与当前登陆的用户一致 * 相当于使用Spring Security的@PreAuthorize("#{user.name == authentication.name}")的验证 * * @author icyfenix@gmail.com * @date 2020/3/16 11:06 **/ @Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = AccountValidation.AuthenticatedAccountValidator.class) public @interface AuthenticatedAccount { String message() default "不是当前登陆用户"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/ExistsAccount.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.account.validation; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 代表一个用户在数据仓库中是存在的 * * @author icyfenix@gmail.com * @date 2020/3/11 19:51 **/ @Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = AccountValidation.ExistsAccountValidator.class) public @interface ExistsAccount { String message() default "用户不存在"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/NotConflictAccount.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.account.validation; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 表示一个用户的信息是无冲突的 *

* “无冲突”是指该用户的敏感信息与其他用户不重合,譬如将一个注册用户的邮箱,修改成与另外一个已存在的注册用户一致的值,这便是冲突 * * @author icyfenix@gmail.com * @date 2020/3/11 20:19 **/ @Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = AccountValidation.NotConflictAccountValidator.class) public @interface NotConflictAccount { String message() default "用户名称、邮箱、手机号码与现存用户产生重复"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/UniqueAccount.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.account.validation; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 表示一个用户是唯一的 *

* 唯一不仅仅是用户名,还要求手机、邮箱均不允许重复 * * @author icyfenix@gmail.com * @date 2020/3/11 19:49 **/ @Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = AccountValidation.UniqueAccountValidator.class) public @interface UniqueAccount { String message() default "用户名称、邮箱、手机号码均不允许与现存用户重复"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/AuthenticAccount.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth; import com.github.fenixsoft.bookstore.domain.account.Account; import org.springframework.beans.BeanUtils; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.HashSet; /** * 认证用户模型 *

* 用户注册之后,包含其业务属性,如姓名、电话、地址,用于业务发生,存储于Account对象中 * 也包含其用于认证的属性,譬如密码、角色、是否停用,存储于AuthenticAccount对象中 * * @author icyfenix@gmail.com * @date 2020/3/7 20:46 **/ public class AuthenticAccount extends Account implements UserDetails { public AuthenticAccount() { super(); authorities.add(new SimpleGrantedAuthority(Role.USER)); } public AuthenticAccount(Account origin) { this(); BeanUtils.copyProperties(origin, this); if (getId() == 1) { // 由于没有做用户管理功能,默认给系统中第一个用户赋予管理员角色 authorities.add(new SimpleGrantedAuthority(Role.ADMIN)); } } /** * 该用户拥有的授权,譬如读取权限、修改权限、增加权限等等 */ private Collection authorities = new HashSet<>(); @Override public Collection getAuthorities() { return authorities; } public void setAuthorities(Collection authorities) { this.authorities = authorities; } /** * 账号是否过期 */ @Override public boolean isAccountNonExpired() { return true; } /** * 是否锁定 */ @Override public boolean isAccountNonLocked() { return true; } /** * 密码是否过期 */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否被锁定 */ @Override public boolean isEnabled() { return true; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/AuthenticAccountRepository.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth; import com.github.fenixsoft.bookstore.domain.account.AccountRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.common.exceptions.UnauthorizedUserException; import org.springframework.stereotype.Component; import java.util.Optional; /** * 认证用户的数据仓库 * * @author icyfenix@gmail.com * @date 2020/3/8 15:21 **/ @Component public class AuthenticAccountRepository { @Autowired private AccountRepository databaseUserRepo; public AuthenticAccount findByUsername(String username) { return new AuthenticAccount(Optional.ofNullable(databaseUserRepo.findByUsername(username)).orElseThrow(() -> new UsernameNotFoundException("用户" + username + "不存在"))); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/Role.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth; /** * 角色常量类,目前系统中只有2种角色:用户,管理员 * * @author icyfenix@gmail.com * @date 2020/3/16 11:32 **/ public interface Role { String USER = "ROLE_USER"; String ADMIN = "ROLE_ADMIN"; } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/provider/PreAuthenticatedAuthenticationProvider.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth.provider; import com.github.fenixsoft.bookstore.domain.auth.AuthenticAccount; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException; import javax.inject.Named; /** * 预验证身份认证器 *

* 预验证是指身份已经在其他地方(第三方)确认过 * 预验证器的目的是将第三方身份管理系统集成到具有Spring安全性的Spring应用程序中,在本项目中,用于JWT令牌过期后重刷新时的验证 * 此时只要检查用户是否有停用、锁定、密码过期、账号过期等问题,如果没有,可根据JWT令牌的刷新过期期限,重新给客户端发放访问令牌 * * @author icyfenix@gmail.com * @date 2020/3/10 11:25 * @see Pre-Authentication Scenarios **/ @Named public class PreAuthenticatedAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (authentication.getPrincipal() instanceof UsernamePasswordAuthenticationToken) { AuthenticAccount user = (AuthenticAccount) ((UsernamePasswordAuthenticationToken) authentication.getPrincipal()).getPrincipal(); // 检查用户没有停用、锁定、密码过期、账号过期等问题 // 在本项目中这些功能都未启用,实际上此检查肯定是会通过的,但为了严谨和日后扩展,还是依次进行了检查 if (user.isEnabled() && user.isCredentialsNonExpired() && user.isAccountNonExpired() && user.isCredentialsNonExpired()) { return new PreAuthenticatedAuthenticationToken(user, "", user.getAuthorities()); } else { throw new DisabledException("用户状态不正确"); } } else { throw new PreAuthenticatedCredentialsNotFoundException("预验证失败,传上来的令牌是怎么来的?"); } } /** * 判断该验证器能处理哪些认证 */ @Override public boolean supports(Class clazz) { return clazz.equals(PreAuthenticatedAuthenticationToken.class); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/provider/UsernamePasswordAuthenticationProvider.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth.provider; import com.github.fenixsoft.bookstore.domain.auth.service.AuthenticAccountDetailsService; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import javax.inject.Inject; import javax.inject.Named; /** * 基于用户名、密码的身份认证器 * 该身份认证器会被{@link AuthenticationManager}验证管理器调用 * 验证管理器支持多种验证方式,这里基于用户名、密码的的身份认证是方式之一 * * @author icyfenix@gmail.com * @date 2020/3/7 21:45 */ @Named public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider { @Inject private AuthenticAccountDetailsService authenticAccountDetailsService; @Inject private PasswordEncoder passwordEncoder; /** * 认证处理 *

* 根据用户名查询用户资料,对比资料中加密后的密码 * 结果将返回一个Authentication的实现类(此处为UsernamePasswordAuthenticationToken)则代表认证成功 * 返回null或者抛出AuthenticationException的子类异常则代表认证失败 */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName().toLowerCase(); String password = (String) authentication.getCredentials(); // AuthenticationException的子类定义了多种认证失败的类型,这里仅处“理用户不存在”、“密码不正确”两种 // 用户不存在的话会直接由loadUserByUsername()抛出异常 UserDetails user = authenticAccountDetailsService.loadUserByUsername(username); if (!passwordEncoder.matches(password, user.getPassword())) throw new BadCredentialsException("密码不正确"); // 认证通过,返回令牌 return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities()); } /** * 判断该验证器能处理哪些认证 */ @Override public boolean supports(Class clazz) { return clazz.equals(UsernamePasswordAuthenticationToken.class); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/service/AuthenticAccountDetailsService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth.service; import com.github.fenixsoft.bookstore.domain.auth.AuthenticAccountRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import javax.inject.Inject; import javax.inject.Named; import java.util.Optional; /** * 认证用户信息查询服务 *

* {@link UserDetailsService}接口定义了从外部(数据库、LDAP,任何地方)根据用户名查询到 */ @Named public class AuthenticAccountDetailsService implements UserDetailsService { @Inject private AuthenticAccountRepository accountRepository; /** * 根据用户名查询用户角色、权限等信息 * 如果用户名无法查询到对应的用户,或者权限不满足,请直接抛出{@link UsernameNotFoundException},勿返回null */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return accountRepository.findByUsername(username); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/service/JWTAccessToken.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth.service; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import javax.inject.Inject; import javax.inject.Named; import java.util.HashMap; import java.util.Map; /** * JWT访问令牌 *

* JWT令牌的结构为三部分组成:[令牌头(Header)].[负载信息(Payload)].[签名(Signature)] * 令牌头:定义了令牌的元数据,如令牌采用的签名算法,默认为HMAC SHA256算法 * 负载信息:由签发者自定义的数据,一般会包括过期时间(Expire)、授权范围(Authority)、令牌ID编号(JTI)等 * 签名:签名是使用私钥和头部指定的算法,前两部分进行的数字签名,防止数据被篡改。 * 以上,令牌头和负载均为JSON结构,进行Base64URLEncode之后进行签名,然后用“.”连接,构成令牌报文 *

* Spring Security OAuth2的{@link JwtAccessTokenConverter}提供了令牌的基础结构(令牌头、部分负载,如过期时间、JTI)的转换实现 * 继承此类,在加入自己定义的负载信息即可使用。一般来说负载中至少要告知服务端当前用户是谁,但又不应存放过多信息导致HTTP Header过大,尤其不应存放敏感信息。 * * @author icyfenix@gmail.com * @date 2020/3/9 9:46 */ @Named public class JWTAccessToken extends JwtAccessTokenConverter { // 签名私钥 // 此处内容是我随便写的UUID,按照JWT约定默认是256Bit的,其实任何格式都可以,只是要注意保密,不要公开出去 private static final String JWT_TOKEN_SIGNING_PRIVATE_KEY = "601304E0-8AD4-40B0-BD51-0B432DC47461"; @Inject JWTAccessToken(UserDetailsService userDetailsService) { // 设置签名私钥 setSigningKey(JWT_TOKEN_SIGNING_PRIVATE_KEY); // 设置从资源请求中带上来的JWT令牌转换回安全上下文中的用户信息的查询服务 // 如果不设置该服务,则从JWT令牌获得的Principal就只有一个用户名(令牌中确实就只存了用户名) // 将用户用户信息查询服务提供给默认的令牌转换器,使得转换令牌时自动根据用户名还原出完整的用户对象 // 这方便了后面编码(可以在直接获得登陆用户信息),但也稳定地为每次请求增加了一次(从数据库/缓存)查询,自行取舍 DefaultUserAuthenticationConverter converter = new DefaultUserAuthenticationConverter(); converter.setUserDetailsService(userDetailsService); ((DefaultAccessTokenConverter) getAccessTokenConverter()).setUserTokenConverter(converter); } /** * 增强令牌 * 增强主要就是在令牌的负载中加入额外的信息 */ @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Authentication user = authentication.getUserAuthentication(); String[] authorities = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toArray(String[]::new); Map payLoad = new HashMap<>(); // Spring Security OAuth的JWT令牌默认实现中就加入了一个“user_name”的项存储了当前用户名 // 这里主要是出于演示Payload的用途,以及方便客户端获取(否则客户端要从令牌中解码Base64来获取),设置了一个“username”,两者的内容是一致的 payLoad.put("username", user.getName()); payLoad.put("authorities", authorities); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(payLoad); return super.enhance(accessToken, authentication); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/service/JWTAccessTokenService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth.service; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import javax.inject.Inject; import javax.inject.Named; /** * JWT访问令牌服务 *

* 在此服务中提供了令牌如何存储、携带哪些信息、如何签名、持续多长时间等相关内容的定义 * 令牌服务应当会被授权服务器{@link com.github.fenixsoft.bookstore.infrastructure.configuration.AuthorizationServerConfiguration}注册验证Endpoint时候调用到 * * @author icyfenix@gmail.com * @date 2020/3/8 11:07 **/ @Named public class JWTAccessTokenService extends DefaultTokenServices { /** * 构建JWT令牌,并进行默认的配置 */ @Inject public JWTAccessTokenService(JWTAccessToken token, OAuthClientDetailsService clientService, AuthenticationManager authenticationManager) { // 设置令牌的持久化容器 // 令牌持久化有多种方式,单节点服务可以存放在Session中,集群可以存放在Redis中 // 而JWT是后端无状态、前端存储的解决方案,Token的存储由前端完成 setTokenStore(new JwtTokenStore(token)); // 令牌支持的客户端详情 setClientDetailsService(clientService); // 设置验证管理器,在鉴权的时候需要用到 setAuthenticationManager(authenticationManager); // 定义令牌的额外负载 setTokenEnhancer(token); // access_token有效期,单位:秒,默认12小时 setAccessTokenValiditySeconds(60 * 60 * 3); // refresh_token的有效期,单位:秒, 默认30天 // 这决定了客户端选择“记住当前登录用户”的最长时效,即失效前都不用再请求用户赋权了 setRefreshTokenValiditySeconds(60 * 60 * 24 * 15); // 是否支持refresh_token,默认false setSupportRefreshToken(true); // 是否复用refresh_token,默认为true // 如果为false,则每次请求刷新都会删除旧的refresh_token,创建新的refresh_token setReuseRefreshToken(true); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/auth/service/OAuthClientDetailsService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.auth.service; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.ClientRegistrationException; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Named; /** * OAuth2客户端类型定义 *

* OAuth2支持四种授权模式,这里仅定义了密码模式(Resource Owner Password Credentials Grant)一种 * OAuth2作为开放的(面向不同服务提供商)授权协议,要求用户提供明文用户名、密码的这种“密码模式”并不常用 * 而这里可以采用是因为前端(BookStore FrontEnd)与后端服务是属于同一个服务提供者的,实质上不存在密码会不会被第三方保存的敏感问题 * 如果永远只考虑单体架构、单一服务提供者,则并无引入OAuth的必要,Spring Security的表单认证就能很良好、便捷地解决认证和授权的问题 * 这里使用密码模式来解决,是为了下一阶段演示微服务化后,服务之间鉴权作准备,以便后续扩展以及对比。 * * @author icyfenix@gmail.com * @date 2020/3/7 19:45 **/ @Named public class OAuthClientDetailsService implements ClientDetailsService { /** * 客户端ID * 这里的客户端就是指本项目的前端代码 */ private static final String CLIENT_ID = "bookstore_frontend"; /** * 客户端密钥 * 在OAuth2协议中,ID是可以公开的,密钥应当保密,密钥用以证明当前申请授权的客户端是未被冒充的 */ private static final String CLIENT_SECRET = "bookstore_secret"; @Inject private PasswordEncoder passwordEncoder; private ClientDetailsService clientDetailsService; /** * 构造密码授权模式 *

* 由于实质上只有一个客户端,所以就不考虑存储和客户端的增删改查了,直接在内存中配置出客户端的信息 *

* 授权Endpoint示例: * /oauth/token?grant_type=password & username=#USER# & password=#PWD# & client_id=bookstore_frontend & client_secret=bookstore_secret * 刷新令牌Endpoint示例: * /oauth/token?grant_type=refresh_token & refresh_token=#REFRESH_TOKEN# & client_id=bookstore_frontend & client_secret=bookstore_secret */ @PostConstruct public void init() throws Exception { InMemoryClientDetailsServiceBuilder builder = new InMemoryClientDetailsServiceBuilder(); // 提供客户端ID和密钥,并指定该客户端支持密码授权、刷新令牌两种访问类型 builder.withClient(CLIENT_ID) .secret(passwordEncoder.encode(CLIENT_SECRET)) .scopes("BROWSER") .authorizedGrantTypes("password", "refresh_token"); clientDetailsService = builder.build(); } /** * 外部根据客户端ID查询验证方式 */ @Override public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { return clientDetailsService.loadClientByClientId(clientId); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/Payment.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import com.github.fenixsoft.bookstore.domain.BaseEntity; import com.github.fenixsoft.bookstore.domain.account.Account; import org.springframework.security.core.context.SecurityContextHolder; import javax.persistence.Entity; import java.util.Date; import java.util.UUID; /** * 支付单模型 *

* 就是传到客户端让用户给扫码或者其他别的方式付钱的对象 * * @author icyfenix@gmail.com * @date 2020/3/12 17:07 **/ @Entity public class Payment extends BaseEntity { /** * 支付状态 */ public enum State { /** * 等待支付中 */ WAITING, /** * 已取消 */ CANCEL, /** * 已支付 */ PAYED, /** * 已超时回滚(未支付,并且商品已恢复) */ TIMEOUT } public Payment() { } public Payment(Double totalPrice, Long expires) { setTotalPrice(totalPrice); setExpires(expires); setCreateTime(new Date()); setPayState(State.WAITING); // 下面这两个是随便写的,实际应该根据情况调用支付服务,返回待支付的ID setPayId(UUID.randomUUID().toString()); // 产生支付单的时候一定是有用户的 Account account = (Account) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); setPaymentLink("/pay/modify/" + getPayId() + "?state=PAYED&accountId=" + account.getId()); } private Date createTime; private String payId; private Double totalPrice; private Long expires; private String paymentLink; private State payState; public String getPayId() { return payId; } public void setPayId(String payId) { this.payId = payId; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Long getExpires() { return expires; } public void setExpires(Long expires) { this.expires = expires; } public String getPaymentLink() { return paymentLink; } public void setPaymentLink(String paymentLink) { this.paymentLink = paymentLink; } public Double getTotalPrice() { return totalPrice; } public void setTotalPrice(Double totalPrice) { this.totalPrice = totalPrice; } public State getPayState() { return payState; } public void setPayState(State payState) { this.payState = payState; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/PaymentRepository.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import org.springframework.data.repository.CrudRepository; import java.util.Optional; /** * 支付单数据仓库 * * @author icyfenix@gmail.com * @date 2020/3/12 23:25 **/ public interface PaymentRepository extends CrudRepository { Payment getByPayId(String payId); } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/PaymentService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; import com.github.fenixsoft.bookstore.infrastructure.cache.CacheConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import javax.annotation.Resource; import javax.inject.Inject; import javax.inject.Named; import javax.persistence.EntityNotFoundException; import java.util.Objects; import java.util.Timer; import java.util.TimerTask; /** * 支付单相关的领域服务 * * @author icyfenix@gmail.com * @date 2020/3/12 23:24 **/ @Named public class PaymentService { /** * 默认支付单超时时间:2分钟(缓存TTL时间的一半) */ private static final long DEFAULT_PRODUCT_FROZEN_EXPIRES = CacheConfiguration.SYSTEM_DEFAULT_EXPIRES / 2; private static final Logger log = LoggerFactory.getLogger(PaymentService.class); private final Timer timer = new Timer(); @Inject private StockpileService stockpileService; @Inject private PaymentRepository paymentRepository; @Resource(name = "settlement") private Cache settlementCache; /** * 生成支付单 *

* 根据结算单冻结指定的货物,计算总价,生成支付单 */ public Payment producePayment(Settlement bill) { Double total = bill.getItems().stream().mapToDouble(i -> { stockpileService.frozen(i.getProductId(), i.getAmount()); return bill.productMap.get(i.getProductId()).getPrice() * i.getAmount(); }).sum() + 12; // 12元固定运费,客户端写死的,这里陪着演一下,避免总价对不上 Payment payment = new Payment(total, DEFAULT_PRODUCT_FROZEN_EXPIRES); paymentRepository.save(payment); // 将支付单存入缓存 settlementCache.put(payment.getPayId(), bill); log.info("创建支付订单,总额:{}", payment.getTotalPrice()); return payment; } /** * 完成支付单 *

* 意味着客户已经完成付款,这个方法在正式业务中应当作为三方支付平台的回调,而演示项目就直接由客户端发起调用了 */ public double accomplish(String payId) { synchronized (payId.intern()) { Payment payment = paymentRepository.getByPayId(payId); if (payment.getPayState() == Payment.State.WAITING) { payment.setPayState(Payment.State.PAYED); paymentRepository.save(payment); accomplishSettlement(Payment.State.PAYED, payment.getPayId()); log.info("编号为{}的支付单已处理完成,等待支付", payId); return payment.getTotalPrice(); } else { throw new UnsupportedOperationException("当前订单不允许支付,当前状态为:" + payment.getPayState()); } } } /** * 取消支付单 *

* 客户取消支付单,此时应当立即释放处于冻结状态的库存 * 由于支付单的存储中应该保存而未持久化的购物明细(在Settlement中),所以这步就不做处理了,等2分钟后在触发器中释放 */ public void cancel(String payId) { synchronized (payId.intern()) { Payment payment = paymentRepository.getByPayId(payId); if (payment.getPayState() == Payment.State.WAITING) { payment.setPayState(Payment.State.CANCEL); paymentRepository.save(payment); accomplishSettlement(Payment.State.CANCEL, payment.getPayId()); log.info("编号为{}的支付单已被取消", payId); } else { throw new UnsupportedOperationException("当前订单不允许取消,当前状态为:" + payment.getPayState()); } } } /** * 设置支付单自动冲销解冻的触发器 *

* 如果在触发器超时之后,如果支付单未仍未被支付(状态是WAITING) * 则自动执行冲销,将冻结的库存商品解冻,以便其他人可以购买,并将Payment的状态修改为ROLLBACK。 *

* 注意: * 使用TimerTask意味着节点带有状态,这在分布式应用中是必须明确【反对】的,如以下缺陷: * 1. 如果要考虑支付订单的取消场景,无论支付状态如何,这个TimerTask到时间之后都应当被执行。不应尝试使用TimerTask::cancel来取消任务。 * 因为只有带有上下文状态的节点才能完成取消操作,如果要在集群中这样做,就必须使用支持集群的定时任务(如Quartz)以保证多节点下能够正常取消任务。 * 2. 如果节点被重启、同样会面临到状态的丢失,导致一部分处于冻结的触发器永远无法被执行,所以需要系统启动时根据数据库状态有一个恢复TimeTask的的操作 * 3. 即时只考虑正常支付的情况,真正生产环境中这种代码需要一个支持集群的同步锁(如用Redis实现互斥量),避免解冻支付和该支付单被完成两个事件同时在不同的节点中发生 */ public void setupAutoThawedTrigger(Payment payment) { timer.schedule(new TimerTask() { public void run() { synchronized (payment.getPayId().intern()) { // 使用2分钟之前的Payment到数据库中查出当前的Payment Payment currentPayment = paymentRepository.findById(payment.getId()).orElseThrow(() -> new EntityNotFoundException(payment.getId().toString())); if (currentPayment.getPayState() == Payment.State.WAITING) { log.info("支付单{}当前状态为:WAITING,转变为:TIMEOUT", payment.getId()); accomplishSettlement(Payment.State.TIMEOUT, payment.getPayId()); } } } }, payment.getExpires()); } /** * 根据支付状态,实际调整库存(扣减库存或者解冻) */ private void accomplishSettlement(Payment.State endState, String payId) { Settlement settlement = (Settlement) Objects.requireNonNull(Objects.requireNonNull(settlementCache.get(payId)).get()); settlement.getItems().forEach(i -> { if (endState == Payment.State.PAYED) { stockpileService.decrease(i.getProductId(), i.getAmount()); } else { // 其他状态,无论是TIMEOUT还是CANCEL,都进行解冻 stockpileService.thawed(i.getProductId(), i.getAmount()); } }); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/Stockpile.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import com.github.fenixsoft.bookstore.domain.BaseEntity; import com.github.fenixsoft.bookstore.domain.warehouse.Product; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.OneToOne; /** * 商品库存 * * @author icyfenix@gmail.com * @date 2020/3/12 16:34 **/ @Entity public class Stockpile extends BaseEntity { private Integer amount; private Integer frozen; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") private transient Product product; public Integer getAmount() { return amount; } public void setAmount(Integer amount) { this.amount = amount; } public void frozen(Integer number) { this.amount -= number; this.frozen += number; } public void thawed(Integer number) { frozen(-1 * number); } public void decrease(Integer number) { this.frozen -= number; } public void increase(Integer number) { this.amount += number; } public Product getProduct() { return product; } public void setProduct(Product product) { this.product = product; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/StockpileRepository.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import org.springframework.data.repository.CrudRepository; /** * 库存数据仓库 * * @author icyfenix@gmail.com * @date 2020/3/12 16:36 **/ public interface StockpileRepository extends CrudRepository { } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/StockpileService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javax.persistence.EntityNotFoundException; /** * 商品库存的领域服务 * * @author icyfenix@gmail.com * @date 2020/3/12 20:23 **/ @Named public class StockpileService { private static final Logger log = LoggerFactory.getLogger(StockpileService.class); @Inject private StockpileRepository repository; /** * 根据产品查询库存 */ public Stockpile getByProductId(Integer productId) { return repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); } /** * 货物售出 * 从冻结状态的货物中扣减 */ public void decrease(Integer productId, Integer amount) { Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); stock.decrease(amount); repository.save(stock); log.info("库存出库,商品:{},数量:{}", productId, amount); } /** * 货物增加 * 增加指定数量货物至正常货物状态 */ public void increase(Integer productId, Integer amount) { Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); stock.increase(amount); repository.save(stock); log.info("库存入库,商品:{},数量:{}", productId, amount); } /** * 货物冻结 * 从正常货物中移动指定数量至冻结状态 */ public void frozen(Integer productId, Integer amount) { Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); stock.frozen(amount); repository.save(stock); log.info("冻结库存,商品:{},数量:{}", productId, amount); } /** * 货物解冻 * 从冻结货物中移动指定数量至正常状态 */ public void thawed(Integer productId, Integer amount) { Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); stock.thawed(amount); repository.save(stock); log.info("解冻库存,商品:{},数量:{}", productId, amount); } /** * 设置货物数量 */ public void set(Integer productId, Integer amount) { Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); stock.setAmount(amount); repository.save(stock); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/Wallet.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import com.github.fenixsoft.bookstore.domain.BaseEntity; import com.github.fenixsoft.bookstore.domain.account.Account; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.OneToOne; /** * 用户钱包 * * @author icyfenix@gmail.com * @date 2020/3/12 16:30 **/ @Entity public class Wallet extends BaseEntity { // 这里是偷懒,正式项目中请使用BigDecimal来表示金额 private Double money; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "account_id") private Account account; public Double getMoney() { return money; } public void setMoney(Double money) { this.money = money; } public Account getAccount() { return account; } public void setAccount(Account account) { this.account = account; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/WalletRepository.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import org.springframework.data.repository.CrudRepository; import java.util.Optional; /** * 钱包数据仓库 * * @author icyfenix@gmail.com * @date 2020/3/12 16:35 **/ public interface WalletRepository extends CrudRepository { Optional findByAccountId(Integer accountId); } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/WalletService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment; import com.github.fenixsoft.bookstore.domain.account.Account; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; /** * 用户钱包的领域服务 *

* 由于本工程中冻结、解冻款项的方法是为了在微服务中演示TCC事务所准备的,单体服务中由于与本地事务一同提交,无需用到 * * @author icyfenix@gmail.com * @date 2020/3/12 20:23 **/ @Named public class WalletService { private static final Logger log = LoggerFactory.getLogger(WalletService.class); @Inject private WalletRepository repository; /** * 账户资金减少 */ public void decrease(Integer accountId, Double amount) { Wallet wallet = repository.findByAccountId(accountId).orElseGet(() -> { Wallet newWallet = new Wallet(); Account account = new Account(); account.setId(accountId); newWallet.setMoney(0D); newWallet.setAccount(account); repository.save(newWallet); return newWallet; }); if (wallet.getMoney() > amount) { wallet.setMoney(wallet.getMoney() - amount); repository.save(wallet); log.info("支付成功。用户余额:{},本次消费:{}", wallet.getMoney(), amount); } else { throw new RuntimeException("用户余额不足以支付,请先充值"); } } /** * 账户资金增加(演示程序,没有做充值入口,实际这个方法无用) */ public void increase(Integer accountId, Double amount) { } // 以下两个方法是为TCC事务准备的,在单体架构中不需要实现 /** * 账户资金冻结 * 从正常资金中移动指定数量至冻结状态 */ public void frozen(Integer accountId, Double amount) { } /** * 账户资金解冻 * 从冻结资金中移动指定数量至正常状态 */ public void thawed(Integer accountId, Double amount) { } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/validation/SettlementValidator.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment.validation; import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; import com.github.fenixsoft.bookstore.domain.payment.StockpileService; import javax.inject.Inject; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; /** * 结算单验证器 *

* 结算单能够成功执行的约束是清单中每一项商品的库存量都足够。 *

* 这个验证器的目的不在于保证商品高并发情况(如秒杀活动)下不超卖,而在于避免库存不足时仍可下单。高并发下的超卖是一种“不可重复读”现象 * (即读取过的数据在事务期间被另一个事务改变),如要严谨地避免,需要把数据库的隔离级别从默认的“Read Committed”提升至“Repeatable Read” * 除了MySQL(InnoDB)外,主流的数据库,如Oracle、SQLServer默认都是Read committed,提升隔离级别会显著影响数据库的并发能力。 * * @author icyfenix@gmail.com * @date 2020/3/16 9:02 **/ public class SettlementValidator implements ConstraintValidator { @Inject private StockpileService service; @Override public boolean isValid(Settlement value, ConstraintValidatorContext context) { return value.getItems().stream().noneMatch(i -> service.getByProductId(i.getProductId()).getAmount() < i.getAmount()); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/payment/validation/SufficientStock.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.payment.validation; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 判断结算单中货物存量是充足的 * * @author icyfenix@gmail.com * @date 2020/3/16 8:59 **/ @Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = SettlementValidator.class) public @interface SufficientStock { String message() default "商品库存不足"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/Advertisement.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.warehouse; import com.github.fenixsoft.bookstore.domain.BaseEntity; import javax.persistence.Column; import javax.persistence.Entity; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; /** * 广告对象模型 * * @author icyfenix@gmail.com * @date 2020/3/7 10:49 **/ @Entity public class Advertisement extends BaseEntity { @NotEmpty(message = "广告图片不允许为空") private String image; @NotNull(message = "广告应当有关联的商品") @Column(name = "product_id") private Integer productId; public String getImage() { return image; } public void setImage(String image) { this.image = image; } public Integer getProductId() { return productId; } public void setProductId(Integer productId) { this.productId = productId; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/AdvertisementRepository.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.warehouse; import org.springframework.dao.DataAccessException; import org.springframework.data.repository.CrudRepository; /** * 广告对象数据仓库 * * @author icyfenix@gmail.com * @date 2020/3/7 10:51 **/ public interface AdvertisementRepository extends CrudRepository { Iterable findAll() throws DataAccessException; } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/Product.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.warehouse; import com.github.fenixsoft.bookstore.domain.BaseEntity; import javax.persistence.*; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.util.Set; /** * 商品对象模型 * * @author icyfenix@gmail.com * @date 2020/3/6 10:43 */ @Entity public class Product extends BaseEntity { @NotEmpty(message = "商品名称不允许为空") private String title; @NotNull(message = "商品应当有明确的价格") @Min(value = 0, message = "商品价格最低为零") // 这里是偷懒,正式场合使用BigDecimal来表示金额 private Double price; @Min(value = 0, message = "评分最低为0") @Max(value = 10, message = "评分最高为10") private Float rate; private String description; private String cover; private String detail; @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name = "product_id") private Set specifications; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public Float getRate() { return rate; } public void setRate(Float rate) { this.rate = rate; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getCover() { return cover; } public void setCover(String cover) { this.cover = cover; } public String getDetail() { return detail; } public void setDetail(String detail) { this.detail = detail; } public Set getSpecifications() { return specifications; } public void setSpecifications(Set specifications) { this.specifications = specifications; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/ProductRepository.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.warehouse; import org.springframework.data.repository.CrudRepository; import java.util.Collection; /** * 商品对象数据仓库 * * @author icyfenix@gmail.com * @date 2020/3/6 20:56 **/ public interface ProductRepository extends CrudRepository { Collection findByIdIn(Collection ids); } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/ProductService.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.warehouse; import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; import javax.inject.Inject; import javax.inject.Named; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; /** * 产品领域服务 * * @author icyfenix@gmail.com * @date 2020/3/12 20:58 **/ @Named public class ProductService { @Inject private ProductRepository repository; /** * 根据结算单中货物的ID,填充货物的完整信息到结算单对象上 */ public void replenishProductInformation(Settlement bill) { List ids = bill.getItems().stream().map(Settlement.Item::getProductId).collect(Collectors.toList()); bill.productMap = repository.findByIdIn(ids).stream().collect(Collectors.toMap(Product::getId, Function.identity())); } /** * 获取仓库中所有的货物信息 */ public Iterable getAllProducts() { return repository.findAll(); } /** * 获取仓库中指定的货物信息 */ public Product getProduct(Integer id) { return repository.findById(id).orElse(null); } /** * 创建或者更新产品信息 */ public Product saveProduct(Product product) { return repository.save(product); } /** * 删除指定产品 */ public void removeProduct(Integer id) { repository.deleteById(id); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/Specification.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.domain.warehouse; import com.github.fenixsoft.bookstore.domain.BaseEntity; import javax.persistence.Column; import javax.persistence.Entity; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; /** * 商品规格 * * @author icyfenix@gmail.com * @date 2020/3/6 19:33 **/ @Entity public class Specification extends BaseEntity { @NotEmpty(message = "商品规格名称不允许为空") private String item; @NotEmpty(message = "商品规格内容不允许为空") private String value; @NotNull(message = "商品规格必须归属于指定商品") @Column(name = "product_id") private Integer productId; public String getItem() { return item; } public void setItem(String item) { this.item = item; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public Integer getProductId() { return productId; } public void setProductId(Integer productId) { this.productId = productId; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/cache/CacheConfiguration.java ================================================ package com.github.fenixsoft.bookstore.infrastructure.cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; /** * 为系统提供一些代码上使用的缓存 * * @author icyfenix@gmail.com * @date 2020/4/7 17:38 **/ @Configuration public class CacheConfiguration { /** * 系统默认缓存TTL时间:4分钟 * 一些需要用到缓存的数据,譬如支付单,需要按此数据来规划过期时间 */ public static final long SYSTEM_DEFAULT_EXPIRES = 4 * 60 * 1000; @Bean public CacheManager configCacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(SYSTEM_DEFAULT_EXPIRES, TimeUnit.MILLISECONDS)); return manager; } @Bean(name = "settlement") public Cache getSettlementTTLCache() { return new CaffeineCache("settlement", Caffeine.newBuilder().expireAfterAccess(SYSTEM_DEFAULT_EXPIRES, TimeUnit.MILLISECONDS).build()); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/AuthenticationServerConfiguration.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.configuration; import com.github.fenixsoft.bookstore.domain.auth.provider.PreAuthenticatedAuthenticationProvider; import com.github.fenixsoft.bookstore.domain.auth.provider.UsernamePasswordAuthenticationProvider; import com.github.fenixsoft.bookstore.domain.auth.service.AuthenticAccountDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.PasswordEncoder; /** * Spring Security的用户认证服务器配置 *

* 借用Spring Security作为认证服务器,告知服务器通过怎样的途径去查询用户、加密密码和验证用户真伪 * 我们实际上并不使用Spring Security提供的认证表单,而是选择了前端通过OAuth2的密码模式,在授权过程中同时完成认证 * 由于服务端整套安全机制(方法授权判断、OAuth2密码模式的用户认证、密码的加密算法)仍然是构建在Spring Security基础之上 * 所以我们的认证服务、用户信息服务仍然继承着Spring Security提供的基类,并在这里注册到Spring Security当中 * * @author icyfenix@gmail.com * @date 2020/3/7 19:41 **/ @Configuration @EnableWebSecurity public class AuthenticationServerConfiguration extends WebSecurityConfiguration { @Autowired private AuthenticAccountDetailsService authenticAccountDetailsService; @Autowired private UsernamePasswordAuthenticationProvider userProvider; @Autowired private PreAuthenticatedAuthenticationProvider preProvider; @Autowired private PasswordEncoder encoder; /** * 需要把AuthenticationManager主动暴漏出来 * 以便在授权服务器{@link AuthorizationServerConfiguration}中可以使用它来完成用户名、密码的认证 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 配置Spring Security的安全认证服务 * Spring Security的Web安全设置,将在资源服务器配置{@link ResourceServerConfiguration}中完成 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(authenticAccountDetailsService).passwordEncoder(encoder); auth.authenticationProvider(userProvider); auth.authenticationProvider(preProvider); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/AuthorizationServerConfiguration.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.configuration; import com.github.fenixsoft.bookstore.domain.auth.service.AuthenticAccountDetailsService; import com.github.fenixsoft.bookstore.domain.auth.service.JWTAccessTokenService; import com.github.fenixsoft.bookstore.domain.auth.service.OAuthClientDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; /** * Spring Security OAuth2 授权服务器配置 *

* 在该配置中,设置了授权服务Endpoint的相关信息(端点的位置、请求方法、使用怎样的令牌、支持怎样的客户端) * 以及针对OAuth2的密码模式所需要的用户身份认证服务和用户详情查询服务 * * @author icyfenix@gmail.com * @date 2020/3/7 17:38 **/ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { /** * 令牌服务 */ @Autowired private JWTAccessTokenService tokenService; /** * OAuth2客户端信息服务 */ @Autowired private OAuthClientDetailsService clientService; /** * 认证服务管理器 *

* 一个认证服务管理器里面包含着多个可以从事不同认证类型的认证提供者(Provider) * 认证服务由认证服务器{@link AuthenticationServerConfiguration}定义并提供注入源 */ @Autowired private AuthenticationManager authenticationManager; /** * 用户信息服务 */ @Autowired private AuthenticAccountDetailsService accountService; /** * 配置客户端详情服务 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientService); } /** * 配置授权的服务Endpoint *

* Spring Security OAuth2会根据配置的认证服务、用户详情服务、令牌服务自动生成以下端点: * /oauth/authorize:授权端点 * /oauth/token:令牌端点 * /oauth/confirm_access:用户确认授权提交端点 * /oauth/error:授权服务错误信息端点 * /oauth/check_token:用于资源服务访问的令牌解析端点 * /oauth/token_key:提供公有密匙的端点,如果JWT采用的是非对称加密加密算法,则资源服务其在鉴权时就需要这个公钥来解码 * 如有必要,这些端点可以使用pathMapping()方法来修改它们的位置,使用prefix()方法来设置路径前缀 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoint) { endpoint.authenticationManager(authenticationManager) .userDetailsService(accountService) .tokenServices(tokenService) //控制TokenEndpoint端点请求访问的类型,默认HttpMethod.POST .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); } /** * 配置OAuth2发布出来的Endpoint本身的安全约束 *

* 这些端点的默认访问规则原本是: * 1. 端点开启了HTTP Basic Authentication,通过allowFormAuthenticationForClients()关闭,即允许通过表单来验证 * 2. 端点的访问均为denyAll(),可以在这里通过SpringEL表达式来改变为permitAll() */ @Override public void configure(AuthorizationServerSecurityConfigurer security) { security.allowFormAuthenticationForClients().tokenKeyAccess("permitAll()").checkTokenAccess("permitAll()"); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/JerseyConfiguration.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.configuration; import org.glassfish.jersey.server.ResourceConfig; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Configuration; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.util.ClassUtils; import javax.ws.rs.ApplicationPath; import javax.ws.rs.Path; import javax.ws.rs.ext.Provider; import java.util.Objects; import java.util.stream.Collectors; /** * Jersey服务器配置 *

* 使用Jersey来提供对JAX-RS(JSR 370:Java API for Restful Web Services)的支持 * 这里设置了所有服务的前缀路径“restful”和restful服务资源的包路径 * * @author icyfenix@gmail.com * @date 2020/3/6 21:10 **/ @Configuration @ApplicationPath("/restful") public class JerseyConfiguration extends ResourceConfig { public JerseyConfiguration() { scanPackages("com.github.fenixsoft.bookstore.resource"); scanPackages("com.github.fenixsoft.bookstore.infrastructure.jaxrs"); } /** * Jersey的packages()方法在Jar形式运行下有问题,这里修理一下 */ private void scanPackages(String scanPackage) { ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); scanner.addIncludeFilter(new AnnotationTypeFilter(Path.class)); scanner.addIncludeFilter(new AnnotationTypeFilter(Provider.class)); this.registerClasses(scanner.findCandidateComponents(scanPackage).stream() .map(beanDefinition -> ClassUtils.resolveClassName(Objects.requireNonNull(beanDefinition.getBeanClassName()), this.getClassLoader())) .collect(Collectors.toSet())); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/ResourceServerConfiguration.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.configuration; import com.github.fenixsoft.bookstore.domain.auth.service.JWTAccessTokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import javax.annotation.security.RolesAllowed; /** * 资源服务器配置 *

* 配置资源服务访问权限,主流有两种方式: * 一是在这里通过{@link HttpSecurity}的antMatchers方法集中配置 * 二是启用全局方法级安全支持{@link EnableGlobalMethodSecurity} 在各个资源的访问方法前,通过注解来逐个配置,使用的注解包括有: * JSR 250标准注解{@link RolesAllowed},可完整替代Spring的{@link Secured}功能 * 以及可以使用EL表达式的Spring注解{@link PreAuthorize}、{@link PostAuthorize} * * @author icyfenix@gmail.com * @date 2020/3/7 19:43 **/ @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Autowired private JWTAccessTokenService tokenService; /** * 配置HTTP访问相关的安全选项 */ public void configure(HttpSecurity http) throws Exception { // 基于JWT来绑定用户状态,所以服务端可以是无状态的 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 关闭CSRF(Cross Site Request Forgery)跨站请求伪造的防御 // 因为需要状态存储CSRF Token才能开启该功能 http.csrf().disable(); // 关闭HTTP Header中的X-Frame-Options选项,允许页面在frame标签中打开 http.headers().frameOptions().disable(); // 设置服务的安全规则 http.authorizeRequests().antMatchers("/oauth/**").permitAll(); } @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.tokenServices(tokenService); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/WebSecurityConfiguration.java ================================================ package com.github.fenixsoft.bookstore.infrastructure.configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * Spring Security安全配置 *

* 移除静态资源目录的安全控制,避免Spring Security默认禁止HTTP缓存的行为 * * @author icyfenix@gmail.com * @date 2020/4/8 0:09 **/ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.headers().cacheControl().disable(); } @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/static/**"); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/AccessDeniedExceptionMapper.java ================================================ package com.github.fenixsoft.bookstore.infrastructure.jaxrs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; /** * 用于统一处理在Resource中由于Spring Security授权访问产生的异常信息 * * @author icyfenix@gmail.com * @date 2020/4/7 0:09 **/ @Provider public class AccessDeniedExceptionMapper implements ExceptionMapper { private static final Logger log = LoggerFactory.getLogger(AccessDeniedExceptionMapper.class); @Context private HttpServletRequest request; @Override public Response toResponse(AccessDeniedException exception) { log.warn("越权访问被禁止 {}: {}", request.getMethod(), request.getPathInfo()); return CommonResponse.send(Response.Status.FORBIDDEN, exception.getMessage()); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/BaseExceptionMapper.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.jaxrs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; /** * 用于兜底的全局处理器,如果其他所有的Mapper都不合适,将由此处理把错误带到前端 * * @author icyfenix@gmail.c * @date 2020/3/12 16:43 **/ @Provider public class BaseExceptionMapper implements ExceptionMapper { private static final Logger log = LoggerFactory.getLogger(BaseExceptionMapper.class); @Override public Response toResponse(Throwable exception) { log.error(exception.getMessage(), exception); return CommonResponse.failure(exception.getMessage()); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/CodedMessage.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.jaxrs; import com.fasterxml.jackson.annotation.JsonInclude; /** * 带编码的实体容器 *

* 一般来说REST服务应采用HTTP Status Code带回错误信息编码 * 但很多前端开发都习惯以JSON-RPC的风格处理异常,所以仍然保留这个编码容器 * 用于返回给客户端以形式为“{code,message,data}”的对象格式 * * @author icyfenix@gmail.com * @date 2020/3/6 15:34 */ @JsonInclude(JsonInclude.Include.NON_NULL) public class CodedMessage { /** * 约定的成功标志 */ public static final Integer CODE_SUCCESS = 0; /** * 默认的失败标志,其他失败含义可以自定义 */ public static final Integer CODE_DEFAULT_FAILURE = 1; private Integer code; private String message; private Object data; public CodedMessage(Integer code, String message) { setCode(code); setMessage(message); } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/CommonResponse.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.jaxrs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.function.Consumer; /** * 为了简化编码而设计的HTTP Response对象包装类和工具集 *

* 带有服务状态编码的(带有Code字段的)JavaBean领域对象包装类 * Code字段的通常用于服务消费者判定该请求的业务处理是否成功。 *

* 统一约定: * - 当服务调用正常完成,返回Code一律以0表示 * - 当服务调用产生异常,可自定义不为0的Code值,此时Message字段作为返回客户端的详细信息 * * @author icyfenix@gmail.com * @date 2020/3/6 15:46 **/ public abstract class CommonResponse { private static final Logger log = LoggerFactory.getLogger(CommonResponse.class); /** * 向客户端发送自定义操作信息 */ public static Response send(Response.Status status, String message) { Integer code = status.getFamily() == Response.Status.Family.SUCCESSFUL ? CodedMessage.CODE_SUCCESS : CodedMessage.CODE_DEFAULT_FAILURE; return Response.status(status).type(MediaType.APPLICATION_JSON).entity(new CodedMessage(code, message)).build(); } /** * 向客户端发送操作失败的信息 */ public static Response failure(String message) { return send(Response.Status.INTERNAL_SERVER_ERROR, message); } /** * 向客户端发送操作成功的信息 */ public static Response success(String message) { return send(Response.Status.OK, message); } /** * 向客户端发送操作成功的信息 */ public static Response success() { return send(Response.Status.OK, "操作已成功"); } /** * 执行操作,并根据操作是否成功返回给客户端相应信息 * 封装了在服务端接口中很常见的执行操作,成功返回成功标志、失败返回失败标志的通用操作,用于简化编码 */ public static Response op(Runnable executor) { return op(executor, e -> log.error(e.getMessage(), e)); } /** * 执行操作(带自定义的失败处理),并根据操作是否成功返回给客户端相应信息 * 封装了在服务端接口中很常见的执行操作,成功返回成功标志、失败返回失败标志的通用操作,用于简化编码 */ public static Response op(Runnable executor, Consumer exceptionConsumer) { try { executor.run(); return CommonResponse.success(); } catch (Exception e) { exceptionConsumer.accept(e); return CommonResponse.failure(e.getMessage()); } } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/ViolationExceptionMapper.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.jaxrs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; import java.util.stream.Collectors; /** * 用于统一处理在Resource中由于验证器验证失败而带回客户端的错误信息 * * @author icyfenix@gmail.com * @date 2020/3/10 23:37 **/ @Provider public class ViolationExceptionMapper implements ExceptionMapper { private static final Logger log = LoggerFactory.getLogger(ViolationExceptionMapper.class); @Override public Response toResponse(ConstraintViolationException exception) { log.warn("客户端传入了校验结果为非法的数据", exception); String msg = exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); return CommonResponse.send(Response.Status.BAD_REQUEST, msg); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/infrastructure/utility/Encryption.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.infrastructure.utility; import org.springframework.context.annotation.Bean; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.inject.Named; import java.util.Optional; /** * 默认的加密工具 * * @author icyfenix@gmail.com * @date 2020/3/10 18:02 **/ @Named public class Encryption { /** * 配置认证使用的密码加密算法:BCrypt * 由于在Spring Security很多验证器中都要用到{@link PasswordEncoder}的加密,所以这里要添加@Bean注解发布出去 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 使用默认加密算法进行编码 */ public String encode(CharSequence rawPassword) { return passwordEncoder().encode(Optional.ofNullable(rawPassword).orElse("")); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/resource/AccountResource.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.resource; import com.github.fenixsoft.bookstore.applicaiton.AccountApplicationService; import com.github.fenixsoft.bookstore.domain.account.Account; import com.github.fenixsoft.bookstore.domain.account.validation.AuthenticatedAccount; import com.github.fenixsoft.bookstore.domain.account.validation.NotConflictAccount; import com.github.fenixsoft.bookstore.domain.account.validation.UniqueAccount; import com.github.fenixsoft.bookstore.infrastructure.jaxrs.CommonResponse; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; import javax.inject.Inject; import javax.validation.Valid; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * 用户资源 *

* 对客户端以Restful形式暴露资源,提供对用户资源{@link Account}的管理入口 * * @author icyfenix@gmail.com * @date 2020/3/6 20:52 **/ @Path("/accounts") @Component @CacheConfig(cacheNames = "resource.account") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class AccountResource { @Inject private AccountApplicationService service; /** * 根据用户名称获取用户详情 */ @GET @Path("/{username}") @Cacheable(key = "#username") public Account getUser(@PathParam("username") String username) { return service.findAccountByUsername(username); } /** * 创建新的用户 */ @POST @CacheEvict(key = "#user.username") public Response createUser(@Valid @UniqueAccount Account user) { return CommonResponse.op(() -> service.createAccount(user)); } /** * 更新用户信息 */ @PUT @CacheEvict(key = "#user.username") public Response updateUser(@Valid @AuthenticatedAccount @NotConflictAccount Account user) { return CommonResponse.op(() -> service.updateAccount(user)); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/resource/AdvertisementResource.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.resource; import com.github.fenixsoft.bookstore.domain.warehouse.Advertisement; import com.github.fenixsoft.bookstore.domain.warehouse.AdvertisementRepository; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; /** * 广告相关的资源 * * @author icyfenix@gmail.com * @date 2020/3/7 10:48 **/ @Path("/advertisements") @Component @Produces(MediaType.APPLICATION_JSON) public class AdvertisementResource { @Inject AdvertisementRepository repository; @GET @Cacheable("resource.advertisements") public Iterable getAllAdvertisements() { return repository.findAll(); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/resource/PaymentResource.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.resource; import com.github.fenixsoft.bookstore.applicaiton.payment.PaymentApplicationService; import com.github.fenixsoft.bookstore.domain.account.Account; import com.github.fenixsoft.bookstore.domain.auth.AuthenticAccount; import com.github.fenixsoft.bookstore.domain.auth.Role; import com.github.fenixsoft.bookstore.domain.payment.Payment; import com.github.fenixsoft.bookstore.domain.payment.Stockpile; import com.github.fenixsoft.bookstore.infrastructure.jaxrs.CommonResponse; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import javax.annotation.security.RolesAllowed; import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * 支付单相关的资源 * * @author icyfenix@gmail.com * @date 2020/3/13 12:52 **/ @Path("/pay") @Component @Produces(MediaType.APPLICATION_JSON) public class PaymentResource { @Inject private PaymentApplicationService service; /** * 修改支付单据的状态 */ @PATCH @Path("/{payId}") @RolesAllowed(Role.USER) public Response updatePaymentState(@PathParam("payId") String payId, @QueryParam("state") Payment.State state) { Account account = (Account) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return updatePaymentStateAlias(payId, account.getId(), state); } /** * 修改支付单状态的GET方法别名 * 考虑到该动作要由二维码扫描来触发,只能进行GET请求,所以增加一个别名以便通过二维码调用 * 这个方法原本应该作为银行支付接口的回调,不控制调用权限(谁付款都行),但都认为是购买用户付的款 */ @GET @Path("/modify/{payId}") public Response updatePaymentStateAlias(@PathParam("payId") String payId, @QueryParam("accountId") Integer accountId, @QueryParam("state") Payment.State state) { if (state == Payment.State.PAYED) { return CommonResponse.op(() -> service.accomplishPayment(accountId, payId)); } else { return CommonResponse.op(() -> service.cancelPayment(payId)); } } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/resource/ProductResource.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.resource; import com.github.fenixsoft.bookstore.applicaiton.ProductApplicationService; import com.github.fenixsoft.bookstore.domain.auth.Role; import com.github.fenixsoft.bookstore.domain.payment.Stockpile; import com.github.fenixsoft.bookstore.domain.warehouse.Product; import com.github.fenixsoft.bookstore.infrastructure.jaxrs.CommonResponse; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Component; import javax.annotation.security.RolesAllowed; import javax.inject.Inject; import javax.validation.Valid; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * 产品相关的资源 * * @author icyfenix@gmail.com * @date 2020/3/6 20:52 **/ @Path("/products") @Component @CacheConfig(cacheNames = "resource.product") @Produces(MediaType.APPLICATION_JSON) public class ProductResource { @Inject ProductApplicationService service; /** * 获取仓库中所有的货物信息 */ @GET @Cacheable(key = "'ALL_PRODUCT'") public Iterable getAllProducts() { return service.getAllProducts(); } /** * 获取仓库中指定的货物信息 */ @GET @Path("/{id}") @Cacheable(key = "#id") public Product getProduct(@PathParam("id") Integer id) { return service.getProduct(id); } /** * 更新产品信息 */ @PUT @Caching(evict = { @CacheEvict(key = "#product.id"), @CacheEvict(key = "'ALL_PRODUCT'") }) @RolesAllowed(Role.ADMIN) public Response updateProduct(@Valid Product product) { return CommonResponse.op(() -> service.saveProduct(product)); } /** * 创建新的产品 */ @POST @Caching(evict = { @CacheEvict(key = "#product.id"), @CacheEvict(key = "'ALL_PRODUCT'") }) @RolesAllowed(Role.ADMIN) public Product createProduct(@Valid Product product) { return service.saveProduct(product); } /** * 删除新的产品 */ @DELETE @Path("/{id}") @Caching(evict = { @CacheEvict(key = "#id"), @CacheEvict(key = "'ALL_PRODUCT'") }) @RolesAllowed(Role.ADMIN) public Response removeProduct(@PathParam("id") Integer id) { return CommonResponse.op(() -> service.removeProduct(id)); } /** * 将指定的产品库存调整为指定数额 */ @PATCH @Path("/stockpile/{productId}") @RolesAllowed(Role.ADMIN) public Response updateStockpile(@PathParam("productId") Integer productId, @QueryParam("amount") Integer amount) { return CommonResponse.op(() -> service.setStockpileAmountByProductId(productId, amount)); } /** * 根据产品查询库存 */ @GET @Path("/stockpile/{productId}") @RolesAllowed(Role.ADMIN) public Stockpile queryStockpile(@PathParam("productId") Integer productId) { return service.getStockpile(productId); } } ================================================ FILE: src/main/java/com/github/fenixsoft/bookstore/resource/SettlementResource.java ================================================ /* * Copyright 2012-2020. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. More information from: * * https://github.com/fenixsoft */ package com.github.fenixsoft.bookstore.resource; import com.github.fenixsoft.bookstore.applicaiton.payment.PaymentApplicationService; import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; import com.github.fenixsoft.bookstore.domain.auth.Role; import com.github.fenixsoft.bookstore.domain.payment.Payment; import com.github.fenixsoft.bookstore.domain.payment.validation.SufficientStock; import org.springframework.stereotype.Component; import javax.annotation.security.RolesAllowed; import javax.inject.Inject; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; /** * 结算清单相关的资源 * * @author icyfenix@gmail.com * @date 2020/3/12 11:23 **/ @Path("/settlements") @Component @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class SettlementResource { @Inject private PaymentApplicationService service; /** * 提交一张交易结算单,根据结算单中的物品,生成支付单 */ @POST @RolesAllowed(Role.USER) public Payment executeSettlement(@Valid @SufficientStock Settlement settlement) { return service.executeBySettlement(settlement); } } ================================================ FILE: src/main/resources/application-mysql.yml ================================================ #请在启动参数中加入--spring.profiles.active=mysql以激活本配置文件 database: mysql spring: datasource: url: "jdbc:mysql://mysql_lan:3306/bookstore?useUnicode=true&characterEncoding=utf-8" username: "root" password: "[4321qwer]" initialization-mode: always ================================================ FILE: src/main/resources/application-test.yml ================================================ database: hsqldb spring: datasource: schema: "classpath:db/${database}/schema.sql" data: "classpath:db/${database}/data.sql" sql-script-encoding: UTF-8 jpa: show-sql: true hibernate: ddl-auto: none logging: pattern: console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(-){faint} %clr([%t]){faint} %clr(%-40logger{39}){cyan}[%line]%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" level: root: INFO com.github.fenixsoft: DEBUG ================================================ FILE: src/main/resources/application.yml ================================================ database: hsqldb spring: datasource: schema: "classpath:db/${database}/schema.sql" data: "classpath:db/${database}/data.sql" sql-script-encoding: UTF-8 jpa: show-sql: false hibernate: ddl-auto: none open-in-view: false resources: chain: compressed: true cache: true cache: period: 86400 logging: pattern: console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(-){faint} %clr([%t]){faint} %clr(%-40logger{39}){cyan}[%line]%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" level: root: INFO ================================================ FILE: src/main/resources/banner.txt ================================================ ______ _ ____ __ _____ __ / ____/__ ____ (_) __ / __ )____ ____ / /_/ ___// /_____ ________ / /_ / _ \/ __ \/ / |/_/ / __ / __ \/ __ \/ __/\__ \/ __/ __ \/ ___/ _ \ / __/ / __/ / / / /> < / /_/ / /_/ / /_/ / /_ ___/ / /_/ /_/ / / / __/ /_/ \___/_/ /_/_/_/|_| /_____/\____/\____/\__//____/\__/\____/_/ \___/ https://icyfenix.cn ================================================ FILE: src/main/resources/db/hsqldb/data.sql ================================================ INSERT INTO product VALUES (8, '凤凰架构:构建可靠的大型分布式系统', 0, 0, '

这是一部以“如何构建一套可靠的分布式大型软件系统”为叙事主线的开源文档,是一幅帮助开发人员整理现代软件架构各条分支中繁多知识点的技能地图。文章《什么是“凤凰架构”》详细阐述了这部文档的主旨、目标与名字的来由,文章《如何开始》简述了文档每章讨论的主要话题与内容详略分布

','/static/cover/fenix.png','/static/desc/fenix.jpg'); INSERT INTO product VALUES (1, '深入理解Java虚拟机(第3版)', 129, 9.6, '

这是一部从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典,繁体版在台湾也颇受欢迎。

自2011年上市以来,前两个版本累计印刷36次,销量超过30万册,两家主要网络书店的评论近90000条,内容上近乎零差评,是原创计算机图书领域不可逾越的丰碑,第3版在第2版的基础上做了重大修订,内容更丰富、实战性更强:根据新版JDK对内容进行了全方位的修订和升级,围绕新技术和生产实践新增逾10万字,包含近50%的全新内容,并对第2版中含糊、瑕疵和错误内容进行了修正。

全书一共13章,分为五大部分:

第一部分(第1章)走近Java

系统介绍了Java的技术体系、发展历程、虚拟机家族,以及动手编译JDK,了解这部分内容能对学习JVM提供良好的指引。

第二部分(第2~5章)自动内存管理

详细讲解了Java的内存区域与内存溢出、垃圾收集器与内存分配策略、虚拟机性能监控与故障排除等与自动内存管理相关的内容,以及10余个经典的性能优化案例和优化方法;

第三部分(第6~9章)虚拟机执行子系统

深入分析了虚拟机执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎,以及多个类加载及其执行子系统的实战案例;

第四部分(第10~11章)程序编译与代码优化

详细讲解了程序的前、后端编译与优化,包括前端的易用性优化措施,如泛型、主动装箱拆箱、条件编译等的内容的深入分析;以及后端的性能优化措施,如虚拟机的热点探测方法、HotSpot 的即时编译器、提前编译器,以及各种常见的编译期优化技术;

第五部分(第12~13章)高效并发

主要讲解了Java实现高并发的原理,包括Java的内存模型、线程与协程,以及线程安全和锁优化。

全书以实战为导向,通过大量与实际生产环境相结合的案例分析和展示了解决各种Java技术难题的方案和技巧。

','/static/cover/jvm3.jpg','/static/desc/jvm3.jpg'); INSERT INTO product VALUES (2, '智慧的疆界', 69, 9.1, '

这是一部对人工智能充满敬畏之心的匠心之作,由《深入理解Java虚拟机》作者耗时一年完成,它将带你从奠基人物、历史事件、学术理论、研究成果、技术应用等5个维度全面读懂人工智能。

\n

本书以时间为主线,用专业的知识、通俗的语言、巧妙的内容组织方式,详细讲解了人工智能这个学科的全貌、能解决什么问题、面临怎样的困难、尝试过哪些努力、取得过多少成绩、未来将向何方发展,尽可能消除人工智能的神秘感,把阳春白雪的人工智能从科学的殿堂推向公众面前。

','/static/cover/ai.jpg','/static/desc/ai.jpg'); INSERT INTO product VALUES (3, 'Java虚拟机规范(Java SE 8)', 79, 7.7, '

本书完整而准确地阐释了Java虚拟机各方面的细节,围绕Java虚拟机整体架构、编译器、class文件格式、加载、链接与初始化、指令集等核心主题对Java虚拟机进行全面而深入的分析,深刻揭示Java虚拟机的工作原理。同时,书中不仅完整地讲述了由Java SE 8所引入的新特性,例如对包含默认实现代码的接口方法所做的调用,还讲述了为支持类型注解及方法参数注解而对class文件格式所做的扩展,并阐明了class文件中各属性的含义,以及字节码验证的规则。

','/static/cover/jvms8.jpg',''); INSERT INTO product VALUES (4, '深入理解Java虚拟机(第2版)', 79, 9.0, '

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》内容简介:第1版两年内印刷近10次,4家网上书店的评论近4?000条,98%以上的评论全部为5星级的好评,是整个Java图书领域公认的经典著作和超级畅销书,繁体版在台湾也十分受欢迎。第2版在第1版的基础上做了很大的改进:根据最新的JDK 1.7对全书内容进行了全面的升级和补充;增加了大量处理各种常见JVM问题的技巧和最佳实践;增加了若干与生产环境相结合的实战案例;对第1版中的错误和不足之处的修正;等等。第2版不仅技术更新、内容更丰富,而且实战性更强。

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》共分为五大部分,围绕内存管理、执行子系统、程序编译与优化、高效并发等核心主题对JVM进行了全面而深入的分析,深刻揭示了JVM的工作原理。

第一部分从宏观的角度介绍了整个Java技术体系、Java和JVM的发展历程、模块化,以及JDK的编译,这对理解书中后面内容有重要帮助。

第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见虚拟机监控与故障处理工具的原理和使用方法。

第三部分分析了虚拟机的执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎。

第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果;

第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。

','/static/cover/jvm2.jpg','/static/desc/jvm2.jpg'); INSERT INTO product VALUES (5, 'Java虚拟机规范(Java SE 7)', 69, 8.9, '

本书整合了自1999年《Java虚拟机规范(第2版)》发布以来Java世界所出现的技术变化。另外,还修正了第2版中的许多错误,以及对目前主流Java虚拟机实现来说已经过时的内容。最后还处理了一些Java虚拟机和Java语言概念的模糊之处。

2004年发布的Java SE 5.0版为Java语言带来了翻天覆地的变化,但是对Java虚拟机设计的影响则相对较小。在Java SE 7这个版本中,我们扩充了class文件格式以便支持新的Java语言特性,譬如泛型和变长参数方法等。

','/static/cover/jvms.jpg','/static/desc/jvms.jpg'); INSERT INTO product VALUES (6, '深入理解OSGi', 79, 7.7, '

本书是原创Java技术图书领域继《深入理解Java虚拟机》后的又一实力之作,也是全球首本基于最新OSGi R5.0规范的著作。理论方面,既全面解读了OSGi规范,深刻揭示了OSGi原理,详细讲解了OSGi服务,又系统地介绍了Equinox框架的使用方法,并通过源码分析了该框架的工作机制;实践方面,不仅包含一些典型的案例,还总结了大量的最佳实践,极具实践指导意义。

全书共14章,分4个部分。第一部分(第1章):走近OSGi,主要介绍了什么是OSGi以及为什么要使用OSGi。第二部分(第2~4章):OSGi规范与原理,对最新的OSGi R5.0中的核心规范进行了全面的解读,首先讲解了OSGi模块的建立、描述、依赖关系的处理,然后讲解了Bundle的启动原理和调度管理,最后讲解了与本地及远程服务相关的内容。第三部分:OSGi服务与Equinox应用实践(第5~11章),不仅详细讲解了OSGi服务纲要规范和企业级规范中最常用的几个子规范和服务的技术细节,还通过一个基于Equinox的BBS案例演示了Equinox的使用方法,最重要的是还通过源码分析了Equinox关键功能的实现机制和原理。第四部分:最佳实践(第12~14章),总结了大量关于OSGi的最佳实践,包括从Bundle如何命名、模块划分、依赖关系处理到保持OSGi动态性、管理程序启动顺序、使用API基线管理模块版本等各方面的实践技巧,此外还介绍了Spring DM的原理以及如何在OSGi环节中进行程序测试。

','/static/cover/osgi.jpg','/static/desc/OSGi.jpg'); INSERT INTO product VALUES (7, '深入理解Java虚拟机', 69, 8.6, '

作为一位Java程序员,你是否也曾经想深入理解Java虚拟机,但是却被它的复杂和深奥拒之门外?没关系,本书极尽化繁为简之妙,能带领你在轻松中领略Java虚拟机的奥秘。本书是近年来国内出版的唯一一本与Java虚拟机相关的专著,也是唯一一本同时从核心理论和实际运用这两个角度去探讨Java虚拟机的著作,不仅理论分析得透彻,而且书中包含的典型案例和最佳实践也极具现实指导意义。

全书共分为五大部分。第一部分从宏观的角度介绍了整个Java技术体系的过去、现在和未来,以及如何独立地编译一个OpenJDK7,这对理解后面的内容很有帮助。第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见的虚拟机的监控与调试工具的原理和使用方法。第三部分分析了虚拟机的执行子系统,包括Class的文件结构以及如何存储和访问Class中的数据;虚拟机的类创建机制以及类加载器的工作原理和它对虚拟机的意义;虚拟机字节码的执行引擎以及它在实行代码时涉及的内存结构。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。

','/static/cover/jvm1.jpg',''); INSERT INTO specification VALUES (1, '作者','周志明',1); INSERT INTO specification VALUES (2, '副标题','JVM高级特性与最佳实践',1); INSERT INTO specification VALUES (3, 'ISBN','9787111641247',1); INSERT INTO specification VALUES (4, '书名','深入理解Java虚拟机(第3版)',1); INSERT INTO specification VALUES (5, '页数', '540',1); INSERT INTO specification VALUES (6, '丛书','华章原创精品',1); INSERT INTO specification VALUES (7, '出版社','机械工业出版社',1); INSERT INTO specification VALUES (8, '出版年','2019-12',1); INSERT INTO specification VALUES (9, '装帧','平装',1); INSERT INTO specification VALUES (10, '作者','周志明',2); INSERT INTO specification VALUES (11, 'ISBN','9787111610496',2); INSERT INTO specification VALUES (12, '书名','智慧的疆界',2); INSERT INTO specification VALUES (13, '副标题','从图灵机到人工智能',2); INSERT INTO specification VALUES (14, '页数','413',2); INSERT INTO specification VALUES (15, '出版社','机械工业出版社',2); INSERT INTO specification VALUES (16, '出版年','2018-1-1',2); INSERT INTO specification VALUES (17, '装帧','平装',2); INSERT INTO specification VALUES (18, '作者','Tim Lindholm / Frank Yellin 等',3); INSERT INTO specification VALUES (19, '译者','爱飞翔 / 周志明 / 等 ',3); INSERT INTO specification VALUES (20, '原作名','The Java Virtual Machine Specification, Java SE 8 Edition',3); INSERT INTO specification VALUES (21, '丛书','Java核心技术系列',3); INSERT INTO specification VALUES (22, 'ISBN','9787111501596',3); INSERT INTO specification VALUES (23, '页数','330',3); INSERT INTO specification VALUES (24, '出版社','机械工业出版社',3); INSERT INTO specification VALUES (25, '出版年','2015-6',3); INSERT INTO specification VALUES (26, '装帧','平装',3) INSERT INTO specification VALUES (27, '作者','周志明',4); INSERT INTO specification VALUES (28, '副标题','JVM高级特性与最佳实践',4); INSERT INTO specification VALUES (29, 'ISBN','9787111421900',4); INSERT INTO specification VALUES (30, '书名','深入理解Java虚拟机(第2版)',4); INSERT INTO specification VALUES (31, '页数', '433',4); INSERT INTO specification VALUES (32, '丛书','华章原创精品',4); INSERT INTO specification VALUES (33, '出版社','机械工业出版社',4); INSERT INTO specification VALUES (34, '出版年','2013-9-1',4); INSERT INTO specification VALUES (35, '装帧','平装',4); INSERT INTO specification VALUES (36, '作者','Tim Lindholm / Frank Yellin 等',5); INSERT INTO specification VALUES (37, '译者','周志明 / 薛笛 / 吴璞渊 / 冶秀刚',5); INSERT INTO specification VALUES (38, '原作名','The Java Virtual Machine Specification, Java SE 7 Edition',5); INSERT INTO specification VALUES (39, '副标题','从图灵机到人工智能',5); INSERT INTO specification VALUES (40, 'ISBN','9787111445159',5); INSERT INTO specification VALUES (41, '页数','316',5); INSERT INTO specification VALUES (42, '出版社','机械工业出版社',5); INSERT INTO specification VALUES (43, '丛书','Java核心技术系列',5); INSERT INTO specification VALUES (44, '出版年','2014-1',5); INSERT INTO specification VALUES (45, '装帧','平装',5); INSERT INTO specification VALUES (46, '作者','周志明 / 谢小明 ',6); INSERT INTO specification VALUES (47, '副标题','Equinox原理、应用与最佳实践',6); INSERT INTO specification VALUES (48, 'ISBN','9787111408871',6); INSERT INTO specification VALUES (49, '书名','智慧的疆界',6); INSERT INTO specification VALUES (50, '丛书','华章原创精品',6); INSERT INTO specification VALUES (51, '页数','432',6); INSERT INTO specification VALUES (52, '出版社','机械工业出版社',6); INSERT INTO specification VALUES (53, '出版年','2013-2-25',6); INSERT INTO specification VALUES (54, '装帧','平装',6); INSERT INTO specification VALUES (55, '作者','周志明',7); INSERT INTO specification VALUES (56, '副标题','JVM高级特性与最佳实践',7); INSERT INTO specification VALUES (57, 'ISBN','9787111349662',7); INSERT INTO specification VALUES (58, '书名','深入理解Java虚拟机',7); INSERT INTO specification VALUES (59, '页数','387',7); INSERT INTO specification VALUES (60, '出版社','机械工业出版社',7); INSERT INTO specification VALUES (61, '出版年','2011-6',7); INSERT INTO specification VALUES (62, '装帧','平装',7); INSERT INTO specification VALUES (63, '作者','周志明',8); INSERT INTO specification VALUES (64, 'ISBN','9787111349662',8); INSERT INTO specification VALUES (65, '书名','凤凰架构',8); INSERT INTO specification VALUES (70, '副标题', '构建可靠的大型分布式系统',8); INSERT INTO specification VALUES (66, '页数','409',8); INSERT INTO specification VALUES (67, '出版社','机械工业出版社',8); INSERT INTO specification VALUES (68, '出版年','2020-6',8); INSERT INTO specification VALUES (69, '装帧','在线',8); INSERT INTO advertisement VALUES (1, '/static/carousel/fenix2.png',8); INSERT INTO advertisement VALUES (2, '/static/carousel/ai.png',2); INSERT INTO advertisement VALUES (3, '/static/carousel/jvm3.png',1); INSERT INTO stockpile VALUES (1, 30, 0, 1); INSERT INTO stockpile VALUES (2, 30, 0, 2); INSERT INTO stockpile VALUES (3, 30, 0, 3); INSERT INTO stockpile VALUES (4, 30, 0, 4); INSERT INTO stockpile VALUES (5, 30, 0, 5); INSERT INTO stockpile VALUES (6, 30, 0, 6); INSERT INTO stockpile VALUES (7, 30, 0, 7); INSERT INTO stockpile VALUES (8, 30, 0, 8); INSERT INTO account VALUES (1, 'icyfenix', '$2a$10$iIim4LtpT2yjxU2YVNDuO.yb1Z2lq86vYBZleAeuIh2aFXjyoMCM.' , '周志明', '', '18888888888', 'icyfenix@gmail.com', '唐家湾港湾大道科技一路3号远光软件股份有限公司'); INSERT INTO wallet VALUES (1, 300, 1); ================================================ FILE: src/main/resources/db/hsqldb/schema.sql ================================================ DROP TABLE wallet IF EXISTS; DROP TABLE account IF EXISTS; DROP TABLE specification IF EXISTS; DROP TABLE advertisement IF EXISTS; DROP TABLE stockpile IF EXISTS; DROP TABLE product IF EXISTS; DROP TABLE payment IF EXISTS; CREATE TABLE account ( id INTEGER IDENTITY PRIMARY KEY, username VARCHAR(50), password VARCHAR(100), name VARCHAR(50), avatar VARCHAR(100), telephone VARCHAR(20), email VARCHAR(100), location VARCHAR(100) ); CREATE UNIQUE INDEX account_user ON account (username); CREATE UNIQUE INDEX account_telephone ON account (telephone); CREATE UNIQUE INDEX account_email ON account (email); CREATE TABLE wallet ( id INTEGER IDENTITY PRIMARY KEY, money DECIMAL, account_id INTEGER ); ALTER TABLE wallet ADD CONSTRAINT fk_wallet_account FOREIGN KEY (account_id) REFERENCES account (id) ON DELETE CASCADE; CREATE TABLE product ( id INTEGER IDENTITY PRIMARY KEY, title VARCHAR(50), price DECIMAL, rate FLOAT, description VARCHAR(8000), cover VARCHAR(100), detail VARCHAR(100) ); CREATE INDEX product_title ON product (title); CREATE TABLE stockpile ( id INTEGER IDENTITY PRIMARY KEY, amount INTEGER, frozen INTEGER, product_id INTEGER ); ALTER TABLE stockpile ADD CONSTRAINT fk_stockpile_product FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE; CREATE TABLE specification ( id INTEGER IDENTITY PRIMARY KEY, item VARCHAR(50), value VARCHAR(100), product_id INTEGER ); ALTER TABLE specification ADD CONSTRAINT fk_specification_product FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE; CREATE TABLE advertisement ( id INTEGER IDENTITY PRIMARY KEY, image VARCHAR(100), product_id INTEGER ); ALTER TABLE advertisement ADD CONSTRAINT fk_advertisement_product FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE; CREATE TABLE payment ( id INTEGER IDENTITY PRIMARY KEY, pay_id VARCHAR(100), create_time DATETIME, total_price DECIMAL, expires INTEGER NOT NULL, payment_link VARCHAR(300), pay_state VARCHAR(20) ); ================================================ FILE: src/main/resources/db/mysql/data.sql ================================================ INSERT INTO product VALUES (8, '凤凰架构:构建可靠的大型分布式系统', 0, 0, '

这是一部以“如何构建一套可靠的分布式大型软件系统”为叙事主线的开源文档,是一幅帮助开发人员整理现代软件架构各条分支中繁多知识点的技能地图。文章《什么是“凤凰架构”》详细阐述了这部文档的主旨、目标与名字的来由,文章《如何开始》简述了文档每章讨论的主要话题与内容详略分布

', '/static/cover/fenix.png', '/static/desc/fenix.jpg'); INSERT INTO product VALUES (1, '深入理解Java虚拟机(第3版)', 129, 9.6, '

这是一部从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典,繁体版在台湾也颇受欢迎。

自2011年上市以来,前两个版本累计印刷36次,销量超过30万册,两家主要网络书店的评论近90000条,内容上近乎零差评,是原创计算机图书领域不可逾越的丰碑,第3版在第2版的基础上做了重大修订,内容更丰富、实战性更强:根据新版JDK对内容进行了全方位的修订和升级,围绕新技术和生产实践新增逾10万字,包含近50%的全新内容,并对第2版中含糊、瑕疵和错误内容进行了修正。

全书一共13章,分为五大部分:

第一部分(第1章)走近Java

系统介绍了Java的技术体系、发展历程、虚拟机家族,以及动手编译JDK,了解这部分内容能对学习JVM提供良好的指引。

第二部分(第2~5章)自动内存管理

详细讲解了Java的内存区域与内存溢出、垃圾收集器与内存分配策略、虚拟机性能监控与故障排除等与自动内存管理相关的内容,以及10余个经典的性能优化案例和优化方法;

第三部分(第6~9章)虚拟机执行子系统

深入分析了虚拟机执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎,以及多个类加载及其执行子系统的实战案例;

第四部分(第10~11章)程序编译与代码优化

详细讲解了程序的前、后端编译与优化,包括前端的易用性优化措施,如泛型、主动装箱拆箱、条件编译等的内容的深入分析;以及后端的性能优化措施,如虚拟机的热点探测方法、HotSpot 的即时编译器、提前编译器,以及各种常见的编译期优化技术;

第五部分(第12~13章)高效并发

主要讲解了Java实现高并发的原理,包括Java的内存模型、线程与协程,以及线程安全和锁优化。

全书以实战为导向,通过大量与实际生产环境相结合的案例分析和展示了解决各种Java技术难题的方案和技巧。

', '/static/cover/jvm3.jpg', '/static/desc/jvm3.jpg'); INSERT INTO product VALUES (2, '智慧的疆界', 69, 9.1, '

这是一部对人工智能充满敬畏之心的匠心之作,由《深入理解Java虚拟机》作者耗时一年完成,它将带你从奠基人物、历史事件、学术理论、研究成果、技术应用等5个维度全面读懂人工智能。

\n

本书以时间为主线,用专业的知识、通俗的语言、巧妙的内容组织方式,详细讲解了人工智能这个学科的全貌、能解决什么问题、面临怎样的困难、尝试过哪些努力、取得过多少成绩、未来将向何方发展,尽可能消除人工智能的神秘感,把阳春白雪的人工智能从科学的殿堂推向公众面前。

', '/static/cover/ai.jpg', '/static/desc/ai.jpg'); INSERT INTO product VALUES (3, 'Java虚拟机规范(Java SE 8)', 79, 7.7, '

本书完整而准确地阐释了Java虚拟机各方面的细节,围绕Java虚拟机整体架构、编译器、class文件格式、加载、链接与初始化、指令集等核心主题对Java虚拟机进行全面而深入的分析,深刻揭示Java虚拟机的工作原理。同时,书中不仅完整地讲述了由Java SE 8所引入的新特性,例如对包含默认实现代码的接口方法所做的调用,还讲述了为支持类型注解及方法参数注解而对class文件格式所做的扩展,并阐明了class文件中各属性的含义,以及字节码验证的规则。

', '/static/cover/jvms8.jpg', ''); INSERT INTO product VALUES (4, '深入理解Java虚拟机(第2版)', 79, 9.0, '

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》内容简介:第1版两年内印刷近10次,4家网上书店的评论近4?000条,98%以上的评论全部为5星级的好评,是整个Java图书领域公认的经典著作和超级畅销书,繁体版在台湾也十分受欢迎。第2版在第1版的基础上做了很大的改进:根据最新的JDK 1.7对全书内容进行了全面的升级和补充;增加了大量处理各种常见JVM问题的技巧和最佳实践;增加了若干与生产环境相结合的实战案例;对第1版中的错误和不足之处的修正;等等。第2版不仅技术更新、内容更丰富,而且实战性更强。

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》共分为五大部分,围绕内存管理、执行子系统、程序编译与优化、高效并发等核心主题对JVM进行了全面而深入的分析,深刻揭示了JVM的工作原理。

第一部分从宏观的角度介绍了整个Java技术体系、Java和JVM的发展历程、模块化,以及JDK的编译,这对理解书中后面内容有重要帮助。

第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见虚拟机监控与故障处理工具的原理和使用方法。

第三部分分析了虚拟机的执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎。

第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果;

第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。

', '/static/cover/jvm2.jpg', '/static/desc/jvm2.jpg'); INSERT INTO product VALUES (5, 'Java虚拟机规范(Java SE 7)', 69, 8.9, '

本书整合了自1999年《Java虚拟机规范(第2版)》发布以来Java世界所出现的技术变化。另外,还修正了第2版中的许多错误,以及对目前主流Java虚拟机实现来说已经过时的内容。最后还处理了一些Java虚拟机和Java语言概念的模糊之处。

2004年发布的Java SE 5.0版为Java语言带来了翻天覆地的变化,但是对Java虚拟机设计的影响则相对较小。在Java SE 7这个版本中,我们扩充了class文件格式以便支持新的Java语言特性,譬如泛型和变长参数方法等。

', '/static/cover/jvms.jpg', '/static/desc/jvms.jpg'); INSERT INTO product VALUES (6, '深入理解OSGi', 79, 7.7, '

本书是原创Java技术图书领域继《深入理解Java虚拟机》后的又一实力之作,也是全球首本基于最新OSGi R5.0规范的著作。理论方面,既全面解读了OSGi规范,深刻揭示了OSGi原理,详细讲解了OSGi服务,又系统地介绍了Equinox框架的使用方法,并通过源码分析了该框架的工作机制;实践方面,不仅包含一些典型的案例,还总结了大量的最佳实践,极具实践指导意义。

全书共14章,分4个部分。第一部分(第1章):走近OSGi,主要介绍了什么是OSGi以及为什么要使用OSGi。第二部分(第2~4章):OSGi规范与原理,对最新的OSGi R5.0中的核心规范进行了全面的解读,首先讲解了OSGi模块的建立、描述、依赖关系的处理,然后讲解了Bundle的启动原理和调度管理,最后讲解了与本地及远程服务相关的内容。第三部分:OSGi服务与Equinox应用实践(第5~11章),不仅详细讲解了OSGi服务纲要规范和企业级规范中最常用的几个子规范和服务的技术细节,还通过一个基于Equinox的BBS案例演示了Equinox的使用方法,最重要的是还通过源码分析了Equinox关键功能的实现机制和原理。第四部分:最佳实践(第12~14章),总结了大量关于OSGi的最佳实践,包括从Bundle如何命名、模块划分、依赖关系处理到保持OSGi动态性、管理程序启动顺序、使用API基线管理模块版本等各方面的实践技巧,此外还介绍了Spring DM的原理以及如何在OSGi环节中进行程序测试。

', '/static/cover/osgi.jpg', '/static/desc/OSGi.jpg'); INSERT INTO product VALUES (7, '深入理解Java虚拟机', 69, 8.6, '

作为一位Java程序员,你是否也曾经想深入理解Java虚拟机,但是却被它的复杂和深奥拒之门外?没关系,本书极尽化繁为简之妙,能带领你在轻松中领略Java虚拟机的奥秘。本书是近年来国内出版的唯一一本与Java虚拟机相关的专著,也是唯一一本同时从核心理论和实际运用这两个角度去探讨Java虚拟机的著作,不仅理论分析得透彻,而且书中包含的典型案例和最佳实践也极具现实指导意义。

全书共分为五大部分。第一部分从宏观的角度介绍了整个Java技术体系的过去、现在和未来,以及如何独立地编译一个OpenJDK7,这对理解后面的内容很有帮助。第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见的虚拟机的监控与调试工具的原理和使用方法。第三部分分析了虚拟机的执行子系统,包括Class的文件结构以及如何存储和访问Class中的数据;虚拟机的类创建机制以及类加载器的工作原理和它对虚拟机的意义;虚拟机字节码的执行引擎以及它在实行代码时涉及的内存结构。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。

', '/static/cover/jvm1.jpg', ''); INSERT INTO specification VALUES (1, '作者', '周志明', 1); INSERT INTO specification VALUES (2, '副标题', 'JVM高级特性与最佳实践', 1); INSERT INTO specification VALUES (3, 'ISBN', '9787111641247', 1); INSERT INTO specification VALUES (4, '书名', '深入理解Java虚拟机(第3版)', 1); INSERT INTO specification VALUES (5, '页数', '540', 1); INSERT INTO specification VALUES (6, '丛书', '华章原创精品', 1); INSERT INTO specification VALUES (7, '出版社', '机械工业出版社', 1); INSERT INTO specification VALUES (8, '出版年', '2019-12', 1); INSERT INTO specification VALUES (9, '装帧', '平装', 1); INSERT INTO specification VALUES (10, '作者', '周志明', 2); INSERT INTO specification VALUES (11, 'ISBN', '9787111610496', 2); INSERT INTO specification VALUES (12, '书名', '智慧的疆界', 2); INSERT INTO specification VALUES (13, '副标题', '从图灵机到人工智能', 2); INSERT INTO specification VALUES (14, '页数', '413', 2); INSERT INTO specification VALUES (15, '出版社', '机械工业出版社', 2); INSERT INTO specification VALUES (16, '出版年', '2018-1-1', 2); INSERT INTO specification VALUES (17, '装帧', '平装', 2); INSERT INTO specification VALUES (18, '作者', 'Tim Lindholm / Frank Yellin 等', 3); INSERT INTO specification VALUES (19, '译者', '爱飞翔 / 周志明 / 等 ', 3); INSERT INTO specification VALUES (20, '原作名', 'The Java Virtual Machine Specification, Java SE 8 Edition', 3); INSERT INTO specification VALUES (21, '丛书', 'Java核心技术系列', 3); INSERT INTO specification VALUES (22, 'ISBN', '9787111501596', 3); INSERT INTO specification VALUES (23, '页数', '330', 3); INSERT INTO specification VALUES (24, '出版社', '机械工业出版社', 3); INSERT INTO specification VALUES (25, '出版年', '2015-6', 3); INSERT INTO specification VALUES (26, '装帧', '平装', 3); INSERT INTO specification VALUES (27, '作者', '周志明', 4); INSERT INTO specification VALUES (28, '副标题', 'JVM高级特性与最佳实践', 4); INSERT INTO specification VALUES (29, 'ISBN', '9787111421900', 4); INSERT INTO specification VALUES (30, '书名', '深入理解Java虚拟机(第2版)', 4); INSERT INTO specification VALUES (31, '页数', '433', 4); INSERT INTO specification VALUES (32, '丛书', '华章原创精品', 4); INSERT INTO specification VALUES (33, '出版社', '机械工业出版社', 4); INSERT INTO specification VALUES (34, '出版年', '2013-9-1', 4); INSERT INTO specification VALUES (35, '装帧', '平装', 4); INSERT INTO specification VALUES (36, '作者', 'Tim Lindholm / Frank Yellin 等', 5); INSERT INTO specification VALUES (37, '译者', '周志明 / 薛笛 / 吴璞渊 / 冶秀刚', 5); INSERT INTO specification VALUES (38, '原作名', 'The Java Virtual Machine Specification, Java SE 7 Edition', 5); INSERT INTO specification VALUES (39, '副标题', '从图灵机到人工智能', 5); INSERT INTO specification VALUES (40, 'ISBN', '9787111445159', 5); INSERT INTO specification VALUES (41, '页数', '316', 5); INSERT INTO specification VALUES (42, '出版社', '机械工业出版社', 5); INSERT INTO specification VALUES (43, '丛书', 'Java核心技术系列', 5); INSERT INTO specification VALUES (44, '出版年', '2014-1', 5); INSERT INTO specification VALUES (45, '装帧', '平装', 5); INSERT INTO specification VALUES (46, '作者', '周志明 / 谢小明 ', 6); INSERT INTO specification VALUES (47, '副标题', 'Equinox原理、应用与最佳实践', 6); INSERT INTO specification VALUES (48, 'ISBN', '9787111408871', 6); INSERT INTO specification VALUES (49, '书名', '智慧的疆界', 6); INSERT INTO specification VALUES (50, '丛书', '华章原创精品', 6); INSERT INTO specification VALUES (51, '页数', '432', 6); INSERT INTO specification VALUES (52, '出版社', '机械工业出版社', 6); INSERT INTO specification VALUES (53, '出版年', '2013-2-25', 6); INSERT INTO specification VALUES (54, '装帧', '平装', 6); INSERT INTO specification VALUES (55, '作者', '周志明', 7); INSERT INTO specification VALUES (56, '副标题', 'JVM高级特性与最佳实践', 7); INSERT INTO specification VALUES (57, 'ISBN', '9787111349662', 7); INSERT INTO specification VALUES (58, '书名', '深入理解Java虚拟机', 7); INSERT INTO specification VALUES (59, '页数', '387', 7); INSERT INTO specification VALUES (60, '出版社', '机械工业出版社', 7); INSERT INTO specification VALUES (61, '出版年', '2011-6', 7); INSERT INTO specification VALUES (62, '装帧', '平装', 7); INSERT INTO specification VALUES (63, '作者', '周志明', 8); INSERT INTO specification VALUES (64, 'ISBN', '9787111349662', 8); INSERT INTO specification VALUES (65, '书名', '凤凰架构', 8); INSERT INTO specification VALUES (70, '副标题', '构建可靠的大型分布式系统', 8); INSERT INTO specification VALUES (66, '页数', '409', 8); INSERT INTO specification VALUES (67, '出版社', '机械工业出版社', 8); INSERT INTO specification VALUES (68, '出版年', '2020-6', 8); INSERT INTO specification VALUES (69, '装帧', '在线', 8); INSERT INTO advertisement VALUES (1, '/static/carousel/fenix2.png', 8); INSERT INTO advertisement VALUES (2, '/static/carousel/ai.png', 2); INSERT INTO advertisement VALUES (3, '/static/carousel/jvm3.png', 1); INSERT INTO stockpile VALUES (1, 30, 0, 1); INSERT INTO stockpile VALUES (2, 30, 0, 2); INSERT INTO stockpile VALUES (3, 30, 0, 3); INSERT INTO stockpile VALUES (4, 30, 0, 4); INSERT INTO stockpile VALUES (5, 30, 0, 5); INSERT INTO stockpile VALUES (6, 30, 0, 6); INSERT INTO stockpile VALUES (7, 30, 0, 7); INSERT INTO stockpile VALUES (8, 30, 0, 8); INSERT INTO account VALUES (1, 'icyfenix', '$2a$10$LTqKTXXRb26SYG3MvFG1UuKhMgc/i6IbUl2RgApiWd39y1EqlXbD6', '周志明', '', '18888888888', 'icyfenix@gmail.com', '唐家湾港湾大道科技一路3号远光软件股份有限公司'); INSERT INTO wallet VALUES (1, 100, 1); ================================================ FILE: src/main/resources/db/mysql/schema.sql ================================================ DROP TABLE IF EXISTS specification; DROP TABLE IF EXISTS advertisement; DROP TABLE IF EXISTS stockpile; DROP TABLE IF EXISTS payment; DROP TABLE IF EXISTS wallet; DROP TABLE IF EXISTS account; DROP TABLE IF EXISTS product; CREATE TABLE IF NOT EXISTS account ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50), password VARCHAR(100), name VARCHAR(50), avatar VARCHAR(100), telephone VARCHAR(20), email VARCHAR(100), location VARCHAR(100), INDEX (username) ) engine = InnoDB; CREATE TABLE IF NOT EXISTS wallet ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, money DECIMAL, account_id INTEGER UNSIGNED, FOREIGN KEY (account_id) REFERENCES account (id) ON DELETE CASCADE ) engine = InnoDB; CREATE TABLE IF NOT EXISTS product ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, title VARCHAR(50), price DECIMAL, rate FLOAT, description VARCHAR(8000), cover VARCHAR(100), detail VARCHAR(100), INDEX (title) ) engine = InnoDB; CREATE TABLE IF NOT EXISTS stockpile ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, amount INTEGER, frozen INTEGER, product_id INTEGER UNSIGNED, FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE ) engine = InnoDB; CREATE TABLE IF NOT EXISTS specification ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, item VARCHAR(50), value VARCHAR(100), product_id INTEGER UNSIGNED, FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE ) engine = InnoDB; CREATE TABLE IF NOT EXISTS advertisement ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, image VARCHAR(100), product_id INTEGER UNSIGNED, FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE ) engine = InnoDB; CREATE TABLE IF NOT EXISTS payment ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, pay_id VARCHAR(100), create_time DATETIME, total_price DECIMAL, expires INTEGER NOT NULL, payment_link VARCHAR(300), pay_state VARCHAR(20) ) engine = InnoDB; ================================================ FILE: src/main/resources/db/mysql/user.sql ================================================ CREATE DATABASE IF NOT EXISTS bookstore; ALTER DATABASE bookstore DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; GRANT ALL PRIVILEGES ON bookstore.* TO 'bookstore@%' IDENTIFIED BY 'bookstore'; ================================================ FILE: src/main/resources/static/index.html ================================================ Fenix's BookStore
================================================ FILE: src/main/resources/static/static/board/gitalk.css ================================================ @font-face { font-family: octicons-link; src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); } .markdown-body { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; line-height: 1.5; color: #24292e; /*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";*/ font-size: 16px; line-height: 1.5; word-wrap: break-word; } .markdown-body .pl-c { color: #6a737d; } .markdown-body .pl-c1, .markdown-body .pl-s .pl-v { color: #005cc5; } .markdown-body .pl-e, .markdown-body .pl-en { color: #6f42c1; } .markdown-body .pl-smi, .markdown-body .pl-s .pl-s1 { color: #24292e; } .markdown-body .pl-ent { color: #22863a; } .markdown-body .pl-k { color: #d73a49; } .markdown-body .pl-s, .markdown-body .pl-pds, .markdown-body .pl-s .pl-pse .pl-s1, .markdown-body .pl-sr, .markdown-body .pl-sr .pl-cce, .markdown-body .pl-sr .pl-sre, .markdown-body .pl-sr .pl-sra { color: #032f62; } .markdown-body .pl-v, .markdown-body .pl-smw { color: #e36209; } .markdown-body .pl-bu { color: #b31d28; } .markdown-body .pl-ii { color: #fafbfc; background-color: #b31d28; } .markdown-body .pl-c2 { color: #fafbfc; background-color: #d73a49; } .markdown-body .pl-c2::before { content: "^M"; } .markdown-body .pl-sr .pl-cce { font-weight: bold; color: #22863a; } .markdown-body .pl-ml { color: #735c0f; } .markdown-body .pl-mh, .markdown-body .pl-mh .pl-en, .markdown-body .pl-ms { font-weight: bold; color: #005cc5; } .markdown-body .pl-mi { font-style: italic; color: #24292e; } .markdown-body .pl-mb { font-weight: bold; color: #24292e; } .markdown-body .pl-md { color: #b31d28; background-color: #ffeef0; } .markdown-body .pl-mi1 { color: #22863a; background-color: #f0fff4; } .markdown-body .pl-mc { color: #e36209; background-color: #ffebda; } .markdown-body .pl-mi2 { color: #f6f8fa; background-color: #005cc5; } .markdown-body .pl-mdr { font-weight: bold; color: #6f42c1; } .markdown-body .pl-ba { color: #586069; } .markdown-body .pl-sg { color: #959da5; } .markdown-body .pl-corl { text-decoration: underline; color: #032f62; } .markdown-body .octicon { display: inline-block; vertical-align: text-top; fill: currentColor; } .markdown-body a { background-color: transparent; -webkit-text-decoration-skip: objects; } .markdown-body a:active, .markdown-body a:hover { outline-width: 0; } .markdown-body strong { font-weight: inherit; } .markdown-body strong { font-weight: bolder; } .markdown-body h1 { font-size: 2em; margin: 0.67em 0; } .markdown-body img { border-style: none; } .markdown-body svg:not(:root) { overflow: hidden; } .markdown-body code, .markdown-body kbd, .markdown-body pre { font-family: monospace, monospace; font-size: 1em; } .markdown-body hr { -webkit-box-sizing: content-box; box-sizing: content-box; height: 0; overflow: visible; } .markdown-body input { font: inherit; margin: 0; } .markdown-body input { overflow: visible; } .markdown-body [type="checkbox"] { -webkit-box-sizing: border-box; box-sizing: border-box; padding: 0; } .markdown-body * { -webkit-box-sizing: border-box; box-sizing: border-box; } .markdown-body input { font-family: inherit; font-size: inherit; line-height: inherit; } .markdown-body a { color: #0366d6; text-decoration: none; } .markdown-body a:hover { text-decoration: underline; } .markdown-body strong { font-weight: 600; } .markdown-body hr { height: 0; margin: 15px 0; overflow: hidden; background: transparent; border: 0; border-bottom: 1px solid #dfe2e5; } .markdown-body hr::before { display: table; content: ""; } .markdown-body hr::after { display: table; clear: both; content: ""; } .markdown-body table { border-spacing: 0; border-collapse: collapse; } .markdown-body td, .markdown-body th { padding: 0; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 0; margin-bottom: 0; } .markdown-body h1 { font-size: 32px; font-weight: 600; } .markdown-body h2 { font-size: 24px; font-weight: 600; } .markdown-body h3 { font-size: 20px; font-weight: 600; } .markdown-body h4 { font-size: 16px; font-weight: 600; } .markdown-body h5 { font-size: 14px; font-weight: 600; } .markdown-body h6 { font-size: 12px; font-weight: 600; } .markdown-body p { margin-top: 0; margin-bottom: 10px; } .markdown-body blockquote { margin: 0; } .markdown-body ul, .markdown-body ol { padding-left: 0; margin-top: 0; margin-bottom: 0; } .markdown-body ol ol, .markdown-body ul ol { list-style-type: lower-roman; } .markdown-body ul ul ol, .markdown-body ul ol ol, .markdown-body ol ul ol, .markdown-body ol ol ol { list-style-type: lower-alpha; } .markdown-body dd { margin-left: 0; } .markdown-body code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; } .markdown-body pre { margin-top: 0; margin-bottom: 0; font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; } .markdown-body .octicon { vertical-align: text-bottom; } .markdown-body .pl-0 { padding-left: 0 !important; } .markdown-body .pl-1 { padding-left: 4px !important; } .markdown-body .pl-2 { padding-left: 8px !important; } .markdown-body .pl-3 { padding-left: 16px !important; } .markdown-body .pl-4 { padding-left: 24px !important; } .markdown-body .pl-5 { padding-left: 32px !important; } .markdown-body .pl-6 { padding-left: 40px !important; } .markdown-body::before { display: table; content: ""; } .markdown-body::after { display: table; clear: both; content: ""; } .markdown-body>*:first-child { margin-top: 0 !important; } .markdown-body>*:last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body .anchor { float: left; padding-right: 4px; margin-left: -20px; line-height: 1; } .markdown-body .anchor:focus { outline: none; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre { margin-top: 0; margin-bottom: 16px; } .markdown-body hr { height: 0.25em; padding: 0; margin: 24px 0; background-color: #e1e4e8; border: 0; } .markdown-body blockquote { padding: 0 1em; color: #6a737d; border-left: 0.25em solid #dfe2e5; } .markdown-body blockquote>:first-child { margin-top: 0; } .markdown-body blockquote>:last-child { margin-bottom: 0; } .markdown-body kbd { display: inline-block; padding: 3px 5px; font-size: 11px; line-height: 10px; color: #444d56; vertical-align: middle; background-color: #fafbfc; border: solid 1px #c6cbd1; border-bottom-color: #959da5; border-radius: 3px; -webkit-box-shadow: inset 0 -1px 0 #959da5; box-shadow: inset 0 -1px 0 #959da5; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 24px; margin-bottom: 16px; font-weight: 600; line-height: 1.25; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: #1b1f23; vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body h1 { padding-bottom: 0.3em; font-size: 2em; border-bottom: 1px solid #eaecef; } .markdown-body h2 { padding-bottom: 0.3em; font-size: 1.5em; border-bottom: 1px solid #eaecef; } .markdown-body h3 { font-size: 1.25em; } .markdown-body h4 { font-size: 1em; } .markdown-body h5 { font-size: 0.875em; } .markdown-body h6 { font-size: 0.85em; color: #6a737d; } .markdown-body ul, .markdown-body ol { padding-left: 2em; } .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0; margin-bottom: 0; } .markdown-body li>p { margin-top: 16px; } .markdown-body li+li { margin-top: 0.25em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { padding: 0; margin-top: 16px; font-size: 1em; font-style: italic; font-weight: 600; } .markdown-body dl dd { padding: 0 16px; margin-bottom: 16px; } .markdown-body table { display: block; width: 100%; overflow: auto; } .markdown-body table th { font-weight: 600; } .markdown-body table th, .markdown-body table td { padding: 6px 13px; border: 1px solid #dfe2e5; } .markdown-body table tr { background-color: #fff; border-top: 1px solid #c6cbd1; } .markdown-body table tr:nth-child(2n) { background-color: #f6f8fa; } .markdown-body img { max-width: 100%; -webkit-box-sizing: content-box; box-sizing: content-box; background-color: #fff; } .markdown-body code { padding: 0; padding-top: 0.2em; padding-bottom: 0.2em; margin: 0; font-size: 85%; background-color: rgba(27,31,35,0.05); border-radius: 3px; } .markdown-body code::before, .markdown-body code::after { letter-spacing: -0.2em; content: "\A0"; } .markdown-body pre { word-wrap: normal; } .markdown-body pre>code { padding: 0; margin: 0; font-size: 100%; word-break: normal; white-space: pre; background: transparent; border: 0; } .markdown-body .highlight { margin-bottom: 16px; } .markdown-body .highlight pre { margin-bottom: 0; word-break: normal; } .markdown-body .highlight pre, .markdown-body pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 3px; } .markdown-body pre code { display: inline; max-width: auto; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; } .markdown-body pre code::before, .markdown-body pre code::after { content: normal; } .markdown-body .full-commit .btn-outline:not(:disabled):hover { color: #005cc5; border-color: #005cc5; } .markdown-body kbd { display: inline-block; padding: 3px 5px; font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 10px; color: #444d56; vertical-align: middle; background-color: #fafbfc; border: solid 1px #d1d5da; border-bottom-color: #c6cbd1; border-radius: 3px; -webkit-box-shadow: inset 0 -1px 0 #c6cbd1; box-shadow: inset 0 -1px 0 #c6cbd1; } .markdown-body :checked+.radio-label { position: relative; z-index: 1; border-color: #0366d6; } .markdown-body .task-list-item { list-style-type: none; } .markdown-body .task-list-item+.task-list-item { margin-top: 3px; } .markdown-body .task-list-item input { margin: 0 0.2em 0.25em -1.6em; vertical-align: middle; } .markdown-body hr { border-bottom-color: #eee; } /* variables */ /* functions & mixins */ /* variables - calculated */ /* styles */ .gt-container { -webkit-box-sizing: border-box; box-sizing: border-box; font-size: 16px; /* loader */ /* error */ /* initing */ /* no int */ /* link */ /* meta */ /* popup */ /* header */ /* comments */ /* comment */ } .gt-container * { -webkit-box-sizing: border-box; box-sizing: border-box; } .gt-container a { color: #6190e8; } .gt-container a:hover { color: #81a6ed; border-color: #81a6ed; } .gt-container a.is--active { color: #333; cursor: default !important; } .gt-container a.is--active:hover { color: #333; } .gt-container .hide { display: none !important; } .gt-container .gt-svg { display: inline-block; width: 1em; height: 1em; vertical-align: sub; } .gt-container .gt-svg svg { width: 100%; height: 100%; fill: #6190e8; } .gt-container .gt-ico { display: inline-block; } .gt-container .gt-ico-text { margin-left: 0.3125em; } .gt-container .gt-ico-github { width: 100%; height: 100%; } .gt-container .gt-ico-github .gt-svg { width: 100%; height: 100%; } .gt-container .gt-ico-github svg { fill: inherit; } .gt-container .gt-spinner { position: relative; } .gt-container .gt-spinner::before { content: ''; -webkit-box-sizing: border-box; box-sizing: border-box; position: absolute; top: 3px; width: 0.75em; height: 0.75em; margin-top: -0.1875em; margin-left: -0.375em; border-radius: 50%; border: 1px solid #fff; border-top-color: #6190e8; -webkit-animation: gt-kf-rotate 0.6s linear infinite; animation: gt-kf-rotate 0.6s linear infinite; } .gt-container .gt-loader { position: relative; border: 1px solid #999; -webkit-animation: ease gt-kf-rotate 1.5s infinite; animation: ease gt-kf-rotate 1.5s infinite; display: inline-block; font-style: normal; width: 1.75em; height: 1.75em; line-height: 1.75em; border-radius: 50%; } .gt-container .gt-loader:before { content: ''; position: absolute; display: block; top: 0; left: 50%; margin-top: -0.1875em; margin-left: -0.1875em; width: 0.375em; height: 0.375em; background-color: #999; border-radius: 50%; } .gt-container .gt-avatar { display: inline-block; width: 3.125em; height: 3.125em; } @media (max-width: 479px) { .gt-container .gt-avatar { width: 2em; height: 2em; } } .gt-container .gt-avatar img { width: 100%; height: auto; border-radius: 3px; } .gt-container .gt-avatar-github { width: 3em; height: 3em; } @media (max-width: 479px) { .gt-container .gt-avatar-github { width: 1.875em; height: 1.875em; } } .gt-container .gt-btn { padding: 0.75em 1.25em; display: inline-block; line-height: 1; text-decoration: none; white-space: nowrap; cursor: pointer; border: 1px solid #6190e8; border-radius: 5px; background-color: #6190e8; color: #fff; outline: none; font-size: 0.75em; } .gt-container .gt-btn-text { font-weight: 400; } .gt-container .gt-btn-loading { position: relative; margin-left: 0.5em; display: inline-block; width: 0.75em; height: 1em; vertical-align: top; } .gt-container .gt-btn.is--disable { cursor: not-allowed; opacity: 0.5; } .gt-container .gt-btn-login { margin-right: 0; } .gt-container .gt-btn-preview { background-color: #fff; color: #6190e8; } .gt-container .gt-btn-preview:hover { background-color: #f2f2f2; border-color: #81a6ed; } .gt-container .gt-btn-public:hover { background-color: #81a6ed; border-color: #81a6ed; } .gt-container .gt-error { text-align: center; margin: 0.625em; color: #ff3860; } .gt-container .gt-initing { padding: 1.25em 0; text-align: center; } .gt-container .gt-initing-text { margin: 0.625em auto; font-size: 92%; } .gt-container .gt-no-init { padding: 1.25em 0; text-align: center; } .gt-container .gt-link { border-bottom: 1px dotted #6190e8; } .gt-container .gt-link-counts, .gt-container .gt-link-project { text-decoration: none; } .gt-container .gt-meta { margin: 0 0 1.25em 0; padding: 1em 0; position: relative; border-bottom: 1px solid #e9e9e9; font-size: 1em; position: relative; z-index: 10; } .gt-container .gt-meta:before, .gt-container .gt-meta:after { content: " "; display: table; } .gt-container .gt-meta:after { clear: both; } .gt-container .gt-counts { margin: 0 0.625em 0 0; } .gt-container .gt-user { float: right; margin: 0; font-size: 92%; } .gt-container .gt-user-pic { width: 16px; height: 16px; vertical-align: top; margin-right: 0.5em; } .gt-container .gt-user-inner { display: inline-block; cursor: pointer; } .gt-container .gt-user .gt-ico { margin: 0 0 0 0.3125em; } .gt-container .gt-user .gt-ico svg { fill: inherit; } .gt-container .gt-user .is--poping .gt-ico svg { fill: #6190e8; } .gt-container .gt-version { color: #a1a1a1; margin-left: 0.375em; } .gt-container .gt-copyright { margin: 0 0.9375em 0.5em; border-top: 1px solid #e9e9e9; padding-top: 0.5em; } .gt-container .gt-popup { position: absolute; right: 0; top: 2.375em; background: #fff; display: inline-block; border: 1px solid #e9e9e9; padding: 0.625em 0; font-size: 0.875em; letter-spacing: 0.5px; } .gt-container .gt-popup .gt-action { cursor: pointer; display: block; margin: 0.5em 0; padding: 0 1.125em; position: relative; text-decoration: none; } .gt-container .gt-popup .gt-action.is--active:before { content: ''; width: 0.25em; height: 0.25em; background: #6190e8; position: absolute; left: 0.5em; top: 0.4375em; } .gt-container .gt-header { position: relative; display: -webkit-box; display: -ms-flexbox; display: flex; } .gt-container .gt-header-comment { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; margin-left: 1.25em; } @media (max-width: 479px) { .gt-container .gt-header-comment { margin-left: 0.875em; } } .gt-container .gt-header-textarea { padding: 0.75em; display: block; -webkit-box-sizing: border-box; box-sizing: border-box; width: 100%; min-height: 5.125em; max-height: 15em; border-radius: 5px; border: 1px solid rgba(0,0,0,0.1); font-size: 0.875em; word-wrap: break-word; resize: vertical; background-color: #f6f6f6; outline: none; -webkit-transition: all 0.25s ease; transition: all 0.25s ease; } .gt-container .gt-header-textarea:hover { background-color: #fbfbfb; } .gt-container .gt-header-preview { padding: 0.75em; border-radius: 5px; border: 1px solid rgba(0,0,0,0.1); background-color: #f6f6f6; } .gt-container .gt-header-controls { position: relative; margin: 0.75em 0 0; } .gt-container .gt-header-controls:before, .gt-container .gt-header-controls:after { content: " "; display: table; } .gt-container .gt-header-controls:after { clear: both; } @media (max-width: 479px) { .gt-container .gt-header-controls { margin: 0; } } .gt-container .gt-header-controls-tip { font-size: 0.875em; color: #6190e8; text-decoration: none; vertical-align: sub; } @media (max-width: 479px) { .gt-container .gt-header-controls-tip { display: none; } } .gt-container .gt-header-controls .gt-btn { float: right; margin-left: 1.25em; } @media (max-width: 479px) { .gt-container .gt-header-controls .gt-btn { float: none; width: 100%; margin: 0.75em 0 0; } } .gt-container:after { content: ''; position: fixed; bottom: 100%; left: 0; right: 0; top: 0; opacity: 0; } .gt-container.gt-input-focused { position: relative; } .gt-container.gt-input-focused:after { content: ''; position: fixed; bottom: 0%; left: 0; right: 0; top: 0; background: #000; opacity: 0.6; -webkit-transition: opacity 0.3s, bottom 0s; transition: opacity 0.3s, bottom 0s; z-index: 9999; } .gt-container.gt-input-focused .gt-header-comment { z-index: 10000; } .gt-container .gt-comments { padding-top: 1.25em; } .gt-container .gt-comments-null { text-align: center; } .gt-container .gt-comments-controls { margin: 1.25em 0; text-align: center; } .gt-container .gt-comment { position: relative; padding: 0.625em 0; display: -webkit-box; display: -ms-flexbox; display: flex; } .gt-container .gt-comment-content { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; margin-left: 1.25em; padding: 0.75em 1em; background-color: #f9f9f9; overflow: auto; -webkit-transition: all ease 0.25s; transition: all ease 0.25s; } .gt-container .gt-comment-content:hover { -webkit-box-shadow: 0 0.625em 3.75em 0 #f4f4f4; box-shadow: 0 0.625em 3.75em 0 #f4f4f4; } @media (max-width: 479px) { .gt-container .gt-comment-content { margin-left: 0.875em; padding: 0.625em 0.75em; } } .gt-container .gt-comment-header { margin-bottom: 0.5em; font-size: 0.875em; position: relative; } .gt-container .gt-comment-block-1 { float: right; height: 1.375em; width: 2em; } .gt-container .gt-comment-block-2 { float: right; height: 1.375em; width: 4em; } .gt-container .gt-comment-username { font-weight: 500; color: #6190e8; text-decoration: none; } .gt-container .gt-comment-username:hover { text-decoration: underline; } .gt-container .gt-comment-text { margin-left: 0.5em; color: #a1a1a1; } .gt-container .gt-comment-date { margin-left: 0.5em; color: #a1a1a1; } .gt-container .gt-comment-like, .gt-container .gt-comment-edit, .gt-container .gt-comment-reply { position: absolute; height: 1.375em; } .gt-container .gt-comment-like:hover, .gt-container .gt-comment-edit:hover, .gt-container .gt-comment-reply:hover { cursor: pointer; } .gt-container .gt-comment-like { top: 0; right: 2em; } .gt-container .gt-comment-edit, .gt-container .gt-comment-reply { top: 0; right: 0; } .gt-container .gt-comment-body { color: #333 !important; } .gt-container .gt-comment-body .email-hidden-toggle a { display: inline-block; height: 12px; padding: 0 9px; font-size: 12px; font-weight: 600; line-height: 6px; color: #444d56; text-decoration: none; vertical-align: middle; background: #dfe2e5; border-radius: 1px; } .gt-container .gt-comment-body .email-hidden-toggle a:hover { background-color: #c6cbd1; } .gt-container .gt-comment-body .email-hidden-reply { display: none; white-space: pre-wrap; } .gt-container .gt-comment-body .email-hidden-reply .email-signature-reply { padding: 0 15px; margin: 15px 0; color: #586069; border-left: 4px solid #dfe2e5; } .gt-container .gt-comment-body .email-hidden-reply.expanded { display: block; } .gt-container .gt-comment-admin .gt-comment-content { background-color: #f6f9fe; } @-webkit-keyframes gt-kf-rotate { 0% { -webkit-transform: rotate(0); transform: rotate(0); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes gt-kf-rotate { 0% { -webkit-transform: rotate(0); transform: rotate(0); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } /*# sourceMappingURL=gitalk.css.map*/ ================================================ FILE: src/main/resources/static/static/board/gitalk.html ================================================
================================================ FILE: src/main/resources/static/static/board/gitalk.min.js ================================================ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Gitalk=t():e.Gitalk=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist",t(t.s=82)}([function(e,t){var n=e.exports={version:"2.6.11"};"number"==typeof __e&&(__e=n)},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(39)("wks"),o=n(25),i=n(1).Symbol,a="function"==typeof i;(e.exports=function(e){return r[e]||(r[e]=a&&i[e]||(a?i:o)("Symbol."+e))}).store=r},function(e,t,n){"use strict";function r(e){return"[object Array]"===C.call(e)}function o(e){return void 0===e}function i(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function a(e){return"[object ArrayBuffer]"===C.call(e)}function u(e){return"undefined"!=typeof FormData&&e instanceof FormData}function s(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function l(e){return"number"==typeof e}function f(e){return null!==e&&"object"==typeof e}function p(e){return"[object Date]"===C.call(e)}function d(e){return"[object File]"===C.call(e)}function h(e){return"[object Blob]"===C.call(e)}function m(e){return"[object Function]"===C.call(e)}function v(e){return f(e)&&m(e.pipe)}function y(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function g(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function b(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function w(e,t){if(null!==e&&void 0!==e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n0;)n[r]=arguments[r+2];if(!y(e))return e;var o=e.attributes||e.props,i=G.h(e.nodeName||e.type,o,e.children||o&&o.children),a=[i,t];return n&&n.length?a.push(n):t&&t.children&&a.push(t.children),m(G.cloneElement.apply(void 0,a))}function y(e){return e&&(e instanceof Y||e.$$typeof===H)}function g(e,t){return t._refProxies[e]||(t._refProxies[e]=function(n){t&&t.refs&&(t.refs[e]=n,null===n&&(delete t._refProxies[e],t=null))})}function b(e){var t=e.nodeName,n=e.attributes;if(n&&"string"==typeof t){var r={};for(var o in n)r[o.toLowerCase()]=o;if(r.ondoubleclick&&(n.ondblclick=n[r.ondoubleclick],delete n[r.ondoubleclick]),r.onchange&&("textarea"===t||"input"===t.toLowerCase()&&!/^fil|che|rad/i.test(n.type))){var i=r.oninput||"oninput";n[i]||(n[i]=P([n[i],n[r.onchange]]),delete n[r.onchange])}}}function w(e){var t=e.attributes;if(t){var n=t.className||t.class;n&&(t.className=n)}}function _(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function x(e,t){for(var n in e)if(!(n in t))return!0;for(var r in t)if(e[r]!==t[r])return!0;return!1}function S(e){return e&&e.base||e}function E(){}function C(e){function t(e,t){k(this),I.call(this,e,t,V),T.call(this,e,t)}return e=_({constructor:t},e),e.mixins&&O(e,N(e.mixins)),e.statics&&_(t,e.statics),e.propTypes&&(t.propTypes=e.propTypes),e.defaultProps&&(t.defaultProps=e.defaultProps),e.getDefaultProps&&(t.defaultProps=e.getDefaultProps()),E.prototype=I.prototype,t.prototype=_(new E,e),t.displayName=e.displayName||"Component",t}function N(e){for(var t={},n=0;n1)for(var n=1;n=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t){e.exports=!0},function(e,t,n){var r=n(53),o=n(40);e.exports=Object.keys||function(e){return r(e,o)}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t,n){var r=n(7).f,o=n(12),i=n(2)("toStringTag");e.exports=function(e,t,n){e&&!o(e=n?e:e.prototype,i)&&r(e,i,{configurable:!0,value:t})}},function(e,t,n){n(106);for(var r=n(1),o=n(11),i=n(15),a=n(2)("toStringTag"),u="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),s=0;s0?r:n)(e)}},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(6),o=n(103),i=n(40),a=n(38)("IE_PROTO"),u=function(){},s=function(){var e,t=n(31)("iframe"),r=i.length;for(t.style.display="none",n(55).appendChild(t),t.src="javascript:",e=t.contentWindow.document,e.open(),e.write("