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后端:以单体架构实现
如果你此时并不曾了解过什么是“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)
* 如,新增用户时,判断该用户对象是否允许唯一,在修改用户时,判断该用户是否存在
*
* @author icyfenix@gmail.com
* @date 2020/3/11 14:22
**/
public class AccountValidation
* “无冲突”是指该用户的敏感信息与其他用户不重合,譬如将一个注册用户的邮箱,修改成与另外一个已存在的注册用户一致的值,这便是冲突
*
* @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 extends Payload>[] 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 extends Payload>[] 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
* 预验证是指身份已经在其他地方(第三方)确认过
* 预验证器的目的是将第三方身份管理系统集成到具有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
* 在此服务中提供了令牌如何存储、携带哪些信息、如何签名、持续多长时间等相关内容的定义
* 令牌服务应当会被授权服务器{@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
* 根据结算单冻结指定的货物,计算总价,生成支付单
*/
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
* 由于本工程中冻结、解冻款项的方法是为了在微服务中演示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
* 借用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}的
* 移除静态资源目录的安全控制,避免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
* 一般来说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
* 对客户端以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 这是一部以“如何构建一套可靠的分布式大型软件系统”为叙事主线的开源文档,是一幅帮助开发人员整理现代软件架构各条分支中繁多知识点的技能地图。文章《什么是“凤凰架构”》详细阐述了这部文档的主旨、目标与名字的来由,文章《如何开始》简述了文档每章讨论的主要话题与内容详略分布 这是一部从工作原理和工程实践两个维度深入剖析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技术难题的方案和技巧。 这是一部对人工智能充满敬畏之心的匠心之作,由《深入理解Java虚拟机》作者耗时一年完成,它将带你从奠基人物、历史事件、学术理论、研究成果、技术应用等5个维度全面读懂人工智能。 本书以时间为主线,用专业的知识、通俗的语言、巧妙的内容组织方式,详细讲解了人工智能这个学科的全貌、能解决什么问题、面临怎样的困难、尝试过哪些努力、取得过多少成绩、未来将向何方发展,尽可能消除人工智能的神秘感,把阳春白雪的人工智能从科学的殿堂推向公众面前。 本书完整而准确地阐释了Java虚拟机各方面的细节,围绕Java虚拟机整体架构、编译器、class文件格式、加载、链接与初始化、指令集等核心主题对Java虚拟机进行全面而深入的分析,深刻揭示Java虚拟机的工作原理。同时,书中不仅完整地讲述了由Java SE 8所引入的新特性,例如对包含默认实现代码的接口方法所做的调用,还讲述了为支持类型注解及方法参数注解而对class文件格式所做的扩展,并阐明了class文件中各属性的含义,以及字节码验证的规则。 《深入理解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语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。 本书整合了自1999年《Java虚拟机规范(第2版)》发布以来Java世界所出现的技术变化。另外,还修正了第2版中的许多错误,以及对目前主流Java虚拟机实现来说已经过时的内容。最后还处理了一些Java虚拟机和Java语言概念的模糊之处。 2004年发布的Java SE 5.0版为Java语言带来了翻天覆地的变化,但是对Java虚拟机设计的影响则相对较小。在Java SE 7这个版本中,我们扩充了class文件格式以便支持新的Java语言特性,譬如泛型和变长参数方法等。 本书是原创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环节中进行程序测试。 作为一位Java程序员,你是否也曾经想深入理解Java虚拟机,但是却被它的复杂和深奥拒之门外?没关系,本书极尽化繁为简之妙,能带领你在轻松中领略Java虚拟机的奥秘。本书是近年来国内出版的唯一一本与Java虚拟机相关的专著,也是唯一一本同时从核心理论和实际运用这两个角度去探讨Java虚拟机的著作,不仅理论分析得透彻,而且书中包含的典型案例和最佳实践也极具现实指导意义。 全书共分为五大部分。第一部分从宏观的角度介绍了整个Java技术体系的过去、现在和未来,以及如何独立地编译一个OpenJDK7,这对理解后面的内容很有帮助。第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见的虚拟机的监控与调试工具的原理和使用方法。第三部分分析了虚拟机的执行子系统,包括Class的文件结构以及如何存储和访问Class中的数据;虚拟机的类创建机制以及类加载器的工作原理和它对虚拟机的意义;虚拟机字节码的执行引擎以及它在实行代码时涉及的内存结构。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。 这是一部以“如何构建一套可靠的分布式大型软件系统”为叙事主线的开源文档,是一幅帮助开发人员整理现代软件架构各条分支中繁多知识点的技能地图。文章《什么是“凤凰架构”》详细阐述了这部文档的主旨、目标与名字的来由,文章《如何开始》简述了文档每章讨论的主要话题与内容详略分布 这是一部从工作原理和工程实践两个维度深入剖析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技术难题的方案和技巧。 这是一部对人工智能充满敬畏之心的匠心之作,由《深入理解Java虚拟机》作者耗时一年完成,它将带你从奠基人物、历史事件、学术理论、研究成果、技术应用等5个维度全面读懂人工智能。 本书以时间为主线,用专业的知识、通俗的语言、巧妙的内容组织方式,详细讲解了人工智能这个学科的全貌、能解决什么问题、面临怎样的困难、尝试过哪些努力、取得过多少成绩、未来将向何方发展,尽可能消除人工智能的神秘感,把阳春白雪的人工智能从科学的殿堂推向公众面前。 本书完整而准确地阐释了Java虚拟机各方面的细节,围绕Java虚拟机整体架构、编译器、class文件格式、加载、链接与初始化、指令集等核心主题对Java虚拟机进行全面而深入的分析,深刻揭示Java虚拟机的工作原理。同时,书中不仅完整地讲述了由Java SE 8所引入的新特性,例如对包含默认实现代码的接口方法所做的调用,还讲述了为支持类型注解及方法参数注解而对class文件格式所做的扩展,并阐明了class文件中各属性的含义,以及字节码验证的规则。 《深入理解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语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。 本书整合了自1999年《Java虚拟机规范(第2版)》发布以来Java世界所出现的技术变化。另外,还修正了第2版中的许多错误,以及对目前主流Java虚拟机实现来说已经过时的内容。最后还处理了一些Java虚拟机和Java语言概念的模糊之处。 2004年发布的Java SE 5.0版为Java语言带来了翻天覆地的变化,但是对Java虚拟机设计的影响则相对较小。在Java SE 7这个版本中,我们扩充了class文件格式以便支持新的Java语言特性,譬如泛型和变长参数方法等。 本书是原创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环节中进行程序测试。 作为一位Java程序员,你是否也曾经想深入理解Java虚拟机,但是却被它的复杂和深奥拒之门外?没关系,本书极尽化繁为简之妙,能带领你在轻松中领略Java虚拟机的奥秘。本书是近年来国内出版的唯一一本与Java虚拟机相关的专著,也是唯一一本同时从核心理论和实际运用这两个角度去探讨Java虚拟机的著作,不仅理论分析得透彻,而且书中包含的典型案例和最佳实践也极具现实指导意义。 全书共分为五大部分。第一部分从宏观的角度介绍了整个Java技术体系的过去、现在和未来,以及如何独立地编译一个OpenJDK7,这对理解后面的内容很有帮助。第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见的虚拟机的监控与调试工具的原理和使用方法。第三部分分析了虚拟机的执行子系统,包括Class的文件结构以及如何存储和访问Class中的数据;虚拟机的类创建机制以及类加载器的工作原理和它对虚拟机的意义;虚拟机字节码的执行引擎以及它在实行代码时涉及的内存结构。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。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安全配置
*