commit 478bebe66bd9a7aa410f95419205400a25f1fc50 Author: SerLiunx-ctrl <17689543@qq.com> Date: Wed May 15 17:52:24 2024 +0800 repos init. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e27c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr + +### 测试数据 ### +instances/ +settings.properties + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f2589d --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# DDNS Manager Lite + +## 简介 +这是一个动态域名解析(Dynamic DNS,简称 DDNS)工具,用于自动更新域名解析记录,以适应动态 IP 地址变化的情况。 当然,这是一个简化版本 + +## 功能特性 +- 支持阿里云 DNS 解析 +- 支持腾讯云 DNS 解析(开发中) +- 实例可继承, 用于填写公共参数 +- 自动检测并更新域名解析记录 +- 配置简单,使用方便 +- 计划支持多种数据存储方式(JSON、XML、数据库等) +- 多线程并发处理 + +## 环境要求 +- jre 1.8 或以上版本 +- 操作系统: 任何能运行jre 1.8的操作系统 + +## 如何使用 +1. 下载最新版本的 DDNS 工具 [Release 页面](https://github.com/your-repo/ddns-tool/releases) +2. 配置 `application.yml` 文件, 配置日志、线程相关信息 +3. 按照下方实例配置进行实例的创建, 目前支持`.json`、`.xml`、`.yaml/.yml`的文件 +4. 解压压缩包到你的计算机上, 运行`start.bat` (如果运行失败, 请检查你的Java是否已安装并成功设置环境变量, 或者编辑该文件手动设置Java信息) +5. 工具会定时检测 IP 地址变化,并自动更新域名解析记录 + +## 阿里云示例配置 +```jsmin +//父实例, 注意: 每个文件只能有一个实例信息 +{ + "type": "TENCENT_CLOUD", //实例类型 + "interval": 300, //执行周期(单位秒) + "name": "root-instance" //实例名称 +} +``` + +```jsmin +//子实例 +{ + "type": "ALI_YUN", //实例类型 + "name": "serliunx-aliyun", //实例名称 + "accessKeyId": "xxxxxxxxxxxxxxxxxx", //实例不同, 参数也不同: 阿里云的AKID + "accessKeySecret": "yyyyyyyyyyyyyyy", //实例不同, 参数也不同: 阿里云的AKS + "recordId": "889741081843109888", //实例不同, 参数也不同: 阿里云解析记录的ID + "rr": "main", //主机记录, 如域名serliunx.com -> main.serliunx.com + "type": "A", //记录类型, 详见阿里云的相关文档 + "fatherName": "root-instance" //父实例名称, 这里指定了上方的实例, 继承了interval属性. +} +``` + +## 腾讯云示例配置 +* 暂无 + +## 注意事项 +* 请确保 DNS 解析商的 API 权限已经正确配置,具有修改域名解析记录的权限 +* 配置文件中的敏感信息请妥善保管,不要泄露给他人 + +## 开源许可 +本工具基于 `GPL 3.0` 许可 进行开源,详情请参阅 LICENSE 文件。 + +## 感谢 +- 感谢所有为开源事业做出贡献的开发者们,感谢你们的无私奉献和辛勤努力。 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..037e907 --- /dev/null +++ b/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + com.serliunx.ddns + ddns-manager-lite + 1.0.0 + + + 8 + 8 + UTF-8 + + 2.7.18 + 1.2.12 + 2.17.0 + 1.30 + 13.2.1 + 3.0.14 + 3.1.1002 + + + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${fasterxml.version} + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.aliyun + alibabacloud-alidns20150109 + ${aliyundns.sdk.version} + + + + com.tencentcloudapi + tencentcloud-sdk-java-dnspod + ${tencent.dnspod.sdk.version} + + + + io.github.openfeign + feign-core + ${feign.core.version} + + + junit + junit + 4.13.2 + test + + + + + ddns-manager-${project.version} + + + + org.springframework.boot + spring-boot-maven-plugin + ${boot.plugin.version} + + + + repackage + + + com.serliunx.ddns.BootStrap + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/serliunx/ddns/BootStrap.java b/src/main/java/com/serliunx/ddns/BootStrap.java new file mode 100644 index 0000000..e3165c8 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/BootStrap.java @@ -0,0 +1,34 @@ +package com.serliunx.ddns; + +import com.serliunx.ddns.config.PropertiesConfiguration; +import com.serliunx.ddns.constant.SystemConstants; +import com.serliunx.ddns.core.context.FileInstanceContext; +import com.serliunx.ddns.support.SystemInitializer; +import com.serliunx.ddns.support.SystemSupport; +import org.slf4j.MDC; + +/** + * 启动类 + * @author SerLiunx + * @since 1.0 + */ +public final class BootStrap { + + public static void main(String[] args){ + beforeInit(); + init(); + } + + private static void beforeInit(){ + MDC.put("pid", SystemSupport.getPid()); + } + + private static void init(){ + SystemInitializer systemInitializer = SystemInitializer + .configurer() + .configuration(new PropertiesConfiguration(SystemConstants.USER_SETTINGS_PROPERTIES_PATH)) + .instanceContext(new FileInstanceContext()) + .done(); + systemInitializer.refresh(); + } +} diff --git a/src/main/java/com/serliunx/ddns/config/AbstractConfiguration.java b/src/main/java/com/serliunx/ddns/config/AbstractConfiguration.java new file mode 100644 index 0000000..b506a70 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/config/AbstractConfiguration.java @@ -0,0 +1,127 @@ +package com.serliunx.ddns.config; + +import com.serliunx.ddns.support.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 配置信息的抽象实现, 定义公共逻辑 + * @author SerLiunx + * @since 1.0 + */ +public abstract class AbstractConfiguration implements Configuration { + + private static final Logger log = LoggerFactory.getLogger(AbstractConfiguration.class); + protected final Map valueMap = new LinkedHashMap<>(16); + private final Lock loadLock = new ReentrantLock(); + + public AbstractConfiguration() {} + + @Override + public Integer getInteger(String key) { + Assert.notNull(key); + String v = valueMap.get(key); + return v == null ? null : Integer.valueOf(v); + } + + @Override + public Integer getInteger(String key, Integer defaultValue) { + Assert.notNull(key, defaultValue); + Integer v = getInteger(key); + return v == null ? defaultValue : v; + } + + @Override + public Long getLong(String key) { + Assert.notNull(key); + String v = valueMap.get(key); + return v == null ? null : Long.valueOf(v); + } + + @Override + public Long getLong(String key, Long defaultValue) { + Assert.notNull(key, defaultValue); + Long v = getLong(key); + return v == null ? defaultValue : v; + } + + @Override + public String getString(String key) { + Assert.notNull(key); + return valueMap.get(key); + } + + @Override + public String getString(String key, String defaultValue) { + Assert.notNull(key, defaultValue); + String v = getString(key); + return v == null ? defaultValue : v; + } + + @Override + public Boolean getBoolean(String key) { + Assert.notNull(key); + return Boolean.valueOf(valueMap.get(key)); + } + + @Override + public Boolean getBoolean(String key, Boolean defaultValue) { + Assert.notNull(key, defaultValue); + String value = valueMap.get(key); + return value == null ? defaultValue : Boolean.valueOf(value); + } + + @Override + public void refresh() { + // 刷新配置信息 + refresh0(); + final Boolean needPrint = getBoolean(ConfigurationKeys.KEY_CFG_LOG_ONSTART); + if(needPrint) + printDetails(); + } + + @Override + public > Enum getEnum(Class clazz, String key) { + return null; + } + + /** + * 载入配置信息请加锁 + */ + protected void load(){ + try { + loadLock.lock(); + // 清空原有的配置信息 + valueMap.clear(); + load0(); + }finally { + loadLock.unlock(); + } + } + + /** + * 打印配置信息 + */ + protected void printDetails(){ + log.info("=====配置信息====="); + valueMap.forEach((k, v) -> { + log.info("{} = {}", k, v); + }); + log.info("================="); + } + + /** + * 具体的刷新逻辑 + */ + protected abstract void refresh0(); + + /** + * 载入逻辑 + */ + protected abstract void load0(); +} diff --git a/src/main/java/com/serliunx/ddns/config/Configuration.java b/src/main/java/com/serliunx/ddns/config/Configuration.java new file mode 100644 index 0000000..9b89f93 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/config/Configuration.java @@ -0,0 +1,72 @@ +package com.serliunx.ddns.config; + +import com.serliunx.ddns.support.Refreshable; + +/** + * @author SerLiunx + * @since 1.0 + */ +public interface Configuration extends Refreshable { + + /** + * 获取整数 + * @param key 键 + * @return 整数 + */ + Integer getInteger(String key); + + /** + * 获取整数, 带默认值 + * @param key 键 + * @param defaultValue 默认值 + * @return 整数 + */ + Integer getInteger(String key, Integer defaultValue); + + /** + * 获取长整数 + * @param key 键 + * @return 长整数 + */ + Long getLong(String key); + + /** + * 获取长整数 + * @param key 键 + * @param defaultValue 默认值 + * @return 长整数 + */ + Long getLong(String key, Long defaultValue); + + /** + * 获取字符串 + * @param key 键 + * @return 字符串 + */ + String getString(String key); + + /** + * 获取字符串 + * @param key 键 + * @param defaultValue 默认值 + * @return 字符串 + */ + String getString(String key, String defaultValue); + + /** + * 获取布尔值 + * @param key 键 + * @return 布尔值 + */ + Boolean getBoolean(String key); + + /** + * 获取布尔值 + * @param key 键 + * @param defaultValue 默认值 + * @return 布尔值 + */ + Boolean getBoolean(String key, Boolean defaultValue); + + > Enum getEnum(Class clazz, String key); +} diff --git a/src/main/java/com/serliunx/ddns/config/ConfigurationKeys.java b/src/main/java/com/serliunx/ddns/config/ConfigurationKeys.java new file mode 100644 index 0000000..2d084e2 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/config/ConfigurationKeys.java @@ -0,0 +1,26 @@ +package com.serliunx.ddns.config; + +/** + * 配置文件键常量信息 + * @author SerLiunx + * @since 1.0 + */ +public final class ConfigurationKeys { + + private ConfigurationKeys(){throw new UnsupportedOperationException();} + + /** + * 线程池核心线程数量 + */ + public static final String KEY_THREAD_POOL_CORE_SIZE = "system.pool.core.size"; + + /** + * 启动时是否输出配置信息 + */ + public static final String KEY_CFG_LOG_ONSTART = "system.cfg.log.onstart"; + + /** + * 定时任务周期: 获取最新IP + */ + public static final String KEY_TASK_REFRESH_INTERVAL_IP = "system.task.refresh.interval.ip"; +} diff --git a/src/main/java/com/serliunx/ddns/config/PropertiesConfiguration.java b/src/main/java/com/serliunx/ddns/config/PropertiesConfiguration.java new file mode 100644 index 0000000..0e54778 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/config/PropertiesConfiguration.java @@ -0,0 +1,59 @@ +package com.serliunx.ddns.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * 使用{@link Properties}实现的简单读取键值对形式的配置信息实现 + * @author SerLiunx + * @since 1.0 + */ +public class PropertiesConfiguration extends AbstractConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesConfiguration.class); + + private final String path; + private Properties properties; + + public PropertiesConfiguration(String path) { + this.path = path; + } + + @Override + protected void refresh0() { + this.properties = new Properties(); + InputStream inputStream = null; + try { + inputStream = Files.newInputStream(Paths.get(path)); + properties.load(inputStream); + // 载入配置信息 + load(); + } catch (IOException e) { + LOGGER.error("配置文件读取出现异常 => {}", e.toString()); + }finally { + if(inputStream != null){ + try { + inputStream.close(); + } catch (IOException e) { + LOGGER.error("配置文件资源释放出现异常 => {}", e.getMessage()); + } + } + } + } + + @Override + protected void load0() { + Set> entries = properties.entrySet(); + entries.forEach(e -> { + valueMap.put((String) e.getKey(), (String) e.getValue()); + }); + } +} diff --git a/src/main/java/com/serliunx/ddns/constant/InstanceClasses.java b/src/main/java/com/serliunx/ddns/constant/InstanceClasses.java new file mode 100644 index 0000000..b1618a2 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/constant/InstanceClasses.java @@ -0,0 +1,33 @@ +package com.serliunx.ddns.constant; + +import com.serliunx.ddns.core.instance.AliyunInstance; +import com.serliunx.ddns.core.instance.Instance; +import com.serliunx.ddns.core.instance.TencentInstance; + +import java.util.HashMap; +import java.util.Map; + +/** + * 实例类型集合 + * @author SerLiunx + * @since 1.0 + */ +public final class InstanceClasses { + private InstanceClasses(){throw new UnsupportedOperationException();} + + private static final Map> instanceTypeMap = + new HashMap>(){ + { + put(InstanceType.ALI_YUN, AliyunInstance.class); + put(InstanceType.TENCENT_CLOUD, TencentInstance.class); + } + }; + + public static Class match(InstanceType type){ + return instanceTypeMap.get(type); + } + + public static Class match(String type){ + return instanceTypeMap.get(InstanceType.valueOf(type)); + } +} diff --git a/src/main/java/com/serliunx/ddns/constant/InstanceFileType.java b/src/main/java/com/serliunx/ddns/constant/InstanceFileType.java new file mode 100644 index 0000000..7dac84c --- /dev/null +++ b/src/main/java/com/serliunx/ddns/constant/InstanceFileType.java @@ -0,0 +1,24 @@ +package com.serliunx.ddns.constant; + +/** + * 保存实例的文件类型: XML、JSON等 + * @author SerLiunx + * @since 1.0 + */ +public enum InstanceFileType { + XML(".xml"), + JSON(".json"), + YML(".yml"), + YAML(".yaml"), + ; + + private final String value; + + public String getValue() { + return value; + } + + InstanceFileType(String value) { + this.value = value; + } +} diff --git a/src/main/java/com/serliunx/ddns/constant/InstanceSource.java b/src/main/java/com/serliunx/ddns/constant/InstanceSource.java new file mode 100644 index 0000000..3698b08 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/constant/InstanceSource.java @@ -0,0 +1,33 @@ +package com.serliunx.ddns.constant; + +import static com.serliunx.ddns.constant.SystemConstants.*; + +/** + * 实例来源 + * @author SerLiunx + * @since 1.0 + */ +public enum InstanceSource { + FILE_JSON(JSON_FILE), + FILE_XML(XML_FILE), + FILE_YML(YML), + DATABASE(DATABASE_SQLITE), + + UNKNOWN("未知"), + ; + + /** + * 来源标签 + *
  • 如果是从文件加载的实例信息, 标签即表示文件后缀名 + *
  • 如果是来自数据库, 标签即表示数据库类型 + */ + private final String sourceTag; + + InstanceSource(String sourceTag) { + this.sourceTag = sourceTag; + } + + public String getSourceTag() { + return sourceTag; + } +} diff --git a/src/main/java/com/serliunx/ddns/constant/InstanceType.java b/src/main/java/com/serliunx/ddns/constant/InstanceType.java new file mode 100644 index 0000000..50c9f75 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/constant/InstanceType.java @@ -0,0 +1,26 @@ +package com.serliunx.ddns.constant; + +/** + * 实例类型: 阿里云、华为云、腾讯云等 + * @author SerLiunx + * @since 1.0 + */ +public enum InstanceType { + + /** + * 可继承的实例 + *
  • 比较该类型为可继承的实例 + *
  • 用于实例的某些参数可复用的情况 + */ + INHERITED, + + /** + * 阿里云 + */ + ALI_YUN, + + /** + * 腾讯云 + */ + TENCENT_CLOUD, +} diff --git a/src/main/java/com/serliunx/ddns/constant/SystemConstants.java b/src/main/java/com/serliunx/ddns/constant/SystemConstants.java new file mode 100644 index 0000000..4418d20 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/constant/SystemConstants.java @@ -0,0 +1,68 @@ +package com.serliunx.ddns.constant; + +import java.io.File; + +/** + * 系统常量 + * @author SerLiunx + * @since 1.0 + */ +public final class SystemConstants { + + private SystemConstants(){throw new UnsupportedOperationException();} + + /** + * 保存实例的文件夹 + */ + public static final String INSTANCE_FOLDER_NAME = "instances"; + + /** + * 运行目录 + */ + public static final String USER_DIR = System.getProperty("user.dir"); + + /** + * JSON文件后缀 + */ + public static final String JSON_FILE = ".json"; + + /** + * XML文件后缀 + */ + public static final String XML_FILE = ".xml"; + + /** + * YML文件后缀 + */ + public static final String YML = ".yml"; + + /** + * properties配置文件名称 + */ + public static final String PROPERTIES_FILE = "settings.properties"; + + /** + * sqlite + */ + public static final String DATABASE_SQLITE = "sqlite"; + + /** + * XML格式的实例文件根元素名称 + */ + public static final String XML_ROOT_INSTANCE_NAME = "instance"; + + /** + * 实例类型字段名 + */ + public final static String TYPE_FIELD = "type"; + + /** + * 用户目录下的实例存放位置 + */ + public static final String USER_INSTANCE_DIR = USER_DIR + File.separator + INSTANCE_FOLDER_NAME; + + /** + * 用户目录下的.properties配置文件 + */ + public static final String USER_SETTINGS_PROPERTIES_PATH = USER_DIR + File.separator + PROPERTIES_FILE; +} diff --git a/src/main/java/com/serliunx/ddns/core/InstanceFileFilter.java b/src/main/java/com/serliunx/ddns/core/InstanceFileFilter.java new file mode 100644 index 0000000..dd9c3db --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/InstanceFileFilter.java @@ -0,0 +1,31 @@ +package com.serliunx.ddns.core; + +import java.io.File; +import java.io.FileFilter; + +/** + * 文件过滤器, 用于加载过滤存储在文件中的实例信息时 + * @author SerLiunx + * @since 1.0 + * @see com.serliunx.ddns.core.factory.FileInstanceFactory + */ +public final class InstanceFileFilter implements FileFilter { + + private final String[] fileSuffix; + + public InstanceFileFilter(String[] fileSuffix) { + this.fileSuffix = fileSuffix; + } + + @Override + public boolean accept(File pathname) { + if(!pathname.isFile()) + return false; + for (String suffix : fileSuffix) { + if(pathname.getName().endsWith(suffix)){ + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/serliunx/ddns/core/Priority.java b/src/main/java/com/serliunx/ddns/core/Priority.java new file mode 100644 index 0000000..60e3e7f --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/Priority.java @@ -0,0 +1,18 @@ +package com.serliunx.ddns.core; + +/** + * 定义一个对象的优先级 + *
  • 数字越大, 优先级越小 + * @author SerLiunx + * @since 1.0 + */ +@FunctionalInterface +public interface Priority { + + /** + * 获取该对象的优先级 + *
  • 数字越大, 优先级越小 + * @return 优先级 + */ + int getPriority(); +} diff --git a/src/main/java/com/serliunx/ddns/core/context/AbstractInstanceContext.java b/src/main/java/com/serliunx/ddns/core/context/AbstractInstanceContext.java new file mode 100644 index 0000000..e067044 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/context/AbstractInstanceContext.java @@ -0,0 +1,153 @@ +package com.serliunx.ddns.core.context; + +import com.serliunx.ddns.constant.InstanceType; +import com.serliunx.ddns.core.factory.ListableInstanceFactory; +import com.serliunx.ddns.core.instance.Instance; +import com.serliunx.ddns.support.Assert; +import com.serliunx.ddns.support.Refreshable; +import com.serliunx.ddns.util.ReflectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.serliunx.ddns.util.InstanceUtils.validateInstance; + +/** + * 实例容器的抽象实现, 定义大部分公共逻辑 + * @author SerLiunx + * @since 1.0 + */ +public abstract class AbstractInstanceContext implements InstanceContext, MultipleSourceInstanceContext { + + private static final Logger log = LoggerFactory.getLogger(AbstractInstanceContext.class); + private final Set listableInstanceFactories = new HashSet<>(); + + /** + * 完整的实例信息 + *
  • 作为主要操作对象 + */ + private Map instanceMap; + + /** + * 实例信息缓存, 此时的实例继承关系并不完整 + *
  • 不能作为主要的操作对象 + *
  • 容器一般会在刷新完毕后清空该Map, 具体取决于容器本身 + */ + private Map cacheInstanceMap; + + @Override + public void refresh() { + if(listableInstanceFactories.isEmpty()) + return; + + // 初始化所有实例工厂 + listableInstanceFactories.stream() + .filter(f -> f != this) + .forEach(ListableInstanceFactory::refresh); + // 加载、过滤所有实例 + Set instances = new HashSet<>(); + listableInstanceFactories.forEach(f -> instances.addAll(f.getInstances())); + + // TODO 加载实例, 按照实例工厂的优先级从低到高优先级排, 高优先级的实例会覆盖低优先级的实例信息(如果存在重复的实例信息) + + // 初次载入 + cacheInstanceMap = new HashMap<>(instances.stream().collect(Collectors.toMap(Instance::getName, i -> i))); + Set builtInstances = buildInstances(instances); + + instanceMap = builtInstances.stream().collect(Collectors.toMap(Instance::getName, i -> i)); + + // 调用善后处理钩子函数 + afterRefresh(); + } + + @Override + public boolean addInstance(Instance instance, boolean override) { + validateInstance(instance); + Instance i = instanceMap.get(instance.getName()); + if(override && i != null){ + return false; + } + instanceMap.put(instance.getName(), instance); + return true; + } + + @Override + public void addInstance(Instance instance) { + addInstance(instance, false); + } + + @Override + public Instance getInstance(String instanceName) { + Assert.notNull(instanceName); + final Instance instance = instanceMap.get(instanceName); + Assert.notNull(instance); + return instance; + } + + @Override + public Set getInstances() { + return instanceMap == null ? Collections.emptySet() : new HashSet<>(instanceMap.values()); + } + + @Override + public Map getInstanceOfType(InstanceType type) { + Assert.notNull(instanceMap); + return instanceMap.values() + .stream() + .filter(i -> i.getType().equals(type)) + .collect(Collectors.toMap(Instance::getName, i -> i)); + } + + @Override + public void addListableInstanceFactory(ListableInstanceFactory listableInstanceFactory) { + listableInstanceFactories.add(listableInstanceFactory); + } + + @Override + public Set getListableInstanceFactories() { + return listableInstanceFactories; + } + + /** + * 善后工作 + */ + public abstract void afterRefresh(); + + /** + * 缓存清理 + */ + protected void clearCache(){ + int size = cacheInstanceMap.size(); + cacheInstanceMap.clear(); + log.debug("缓存信息清理 => {} 条", size); + // 清理实例工厂的缓存信息 + listableInstanceFactories.forEach(Refreshable::afterRefresh); + } + + /** + * 构建完整的实例信息 + * @param instances 实例信息 + * @return 属性设置完整的实例 + */ + private Set buildInstances(Collection instances){ + //设置实例信息, 如果需要从父类继承 + return instances.stream() + .filter(i -> !InstanceType.INHERITED.equals(i.getType())) + .peek(i -> { + String fatherName = i.getFatherName(); + if(fatherName != null && !fatherName.isEmpty()){ + Instance fatherInstance = cacheInstanceMap.get(fatherName); + if(fatherInstance != null){ + try { + ReflectionUtils.copyField(fatherInstance, i, true); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + }) + .collect(Collectors.toCollection(HashSet::new)); + } +} diff --git a/src/main/java/com/serliunx/ddns/core/context/FileInstanceContext.java b/src/main/java/com/serliunx/ddns/core/context/FileInstanceContext.java new file mode 100644 index 0000000..4ca54c6 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/context/FileInstanceContext.java @@ -0,0 +1,27 @@ +package com.serliunx.ddns.core.context; + +import com.serliunx.ddns.constant.SystemConstants; +import com.serliunx.ddns.core.factory.JsonFileInstanceFactory; +import com.serliunx.ddns.core.factory.XmlFileInstanceFactory; +import com.serliunx.ddns.core.factory.YamlFileInstanceFactory; + +/** + * 文件形式的实例容器 + * @author SerLiunx + * @since 1.0 + */ +public class FileInstanceContext extends AbstractInstanceContext { + + public FileInstanceContext() { + addListableInstanceFactory(new JsonFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR)); + addListableInstanceFactory(new XmlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR)); + addListableInstanceFactory(new YamlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR)); + // 刷新容器 + refresh(); + } + + @Override + public void afterRefresh() { + clearCache(); + } +} diff --git a/src/main/java/com/serliunx/ddns/core/context/GenericInstanceContext.java b/src/main/java/com/serliunx/ddns/core/context/GenericInstanceContext.java new file mode 100644 index 0000000..a11ef23 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/context/GenericInstanceContext.java @@ -0,0 +1,14 @@ +package com.serliunx.ddns.core.context; + +/** + * 简易的容器实现, 需要手动进行刷新、添加实例工厂. + * @author SerLiunx + * @since 1.0 + */ +public class GenericInstanceContext extends AbstractInstanceContext { + + @Override + public void afterRefresh() { + clearCache(); + } +} diff --git a/src/main/java/com/serliunx/ddns/core/context/InstanceContext.java b/src/main/java/com/serliunx/ddns/core/context/InstanceContext.java new file mode 100644 index 0000000..8b07bf8 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/context/InstanceContext.java @@ -0,0 +1,16 @@ +package com.serliunx.ddns.core.context; + +import com.serliunx.ddns.core.factory.InstanceFactory; +import com.serliunx.ddns.support.Refreshable; + +/** + * @author SerLiunx + * @since 1.0 + */ +public interface InstanceContext extends InstanceFactory, Refreshable { + + @Override + default int getPriority() { + return 0; + } +} diff --git a/src/main/java/com/serliunx/ddns/core/context/MultipleSourceInstanceContext.java b/src/main/java/com/serliunx/ddns/core/context/MultipleSourceInstanceContext.java new file mode 100644 index 0000000..2590877 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/context/MultipleSourceInstanceContext.java @@ -0,0 +1,28 @@ +package com.serliunx.ddns.core.context; + +import com.serliunx.ddns.core.factory.InstanceFactory; +import com.serliunx.ddns.core.factory.ListableInstanceFactory; + +import java.util.Set; + +/** + * 多数据源的实例容器, 将多种实例来源汇聚到一起 + * @see InstanceFactory + * @see InstanceContext + * @author SerLiunx + * @since 1.0 + */ +public interface MultipleSourceInstanceContext extends InstanceContext, ListableInstanceFactory { + + /** + * 添加一个实例工厂 + * @param listableInstanceFactory 实例工厂 + */ + void addListableInstanceFactory(ListableInstanceFactory listableInstanceFactory); + + /** + * 获取所有实例工厂 + * @return 实例工厂列表 + */ + Set getListableInstanceFactories(); +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/AbstractInstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/AbstractInstanceFactory.java new file mode 100644 index 0000000..c1417dc --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/AbstractInstanceFactory.java @@ -0,0 +1,90 @@ +package com.serliunx.ddns.core.factory; + +import com.serliunx.ddns.constant.InstanceType; +import com.serliunx.ddns.core.instance.Instance; +import com.serliunx.ddns.support.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.serliunx.ddns.util.InstanceUtils.validateInstance; + +/** + * @author SerLiunx + * @since 1.0 + */ +public abstract class AbstractInstanceFactory implements InstanceFactory, ListableInstanceFactory { + + private static final Logger log = LoggerFactory.getLogger(AbstractInstanceFactory.class); + + /** + * 实例信息 + */ + private Map instanceMap; + + @Override + public Instance getInstance(String instanceName) { + Assert.notNull(instanceName); + final Instance instance = instanceMap.get(instanceName); + Assert.notNull(instance); + return instance; + } + + @Override + public Set getInstances() { + return instanceMap == null ? Collections.emptySet() : new HashSet<>(instanceMap.values()); + } + + @Override + public Map getInstanceOfType(InstanceType type) { + Assert.notNull(instanceMap); + return instanceMap.values() + .stream() + .filter(i -> i.getType().equals(type)) + .collect(Collectors.toMap(Instance::getName, i -> i)); + } + + @Override + public boolean addInstance(Instance instance, boolean override) { + validateInstance(instance); + Instance i = instanceMap.get(instance.getName()); + if(override && i != null){ + return false; + } + instanceMap.put(instance.getName(), instance); + return true; + } + + @Override + public void addInstance(Instance instance) { + addInstance(instance, false); + } + + @Override + public void refresh() { + Set instances = load(); + if(instances != null && !instances.isEmpty()) + instanceMap = new HashMap<>(instances.stream() + .collect(Collectors.toMap(Instance::getName, i -> i))); + } + + @Override + public int getPriority() { + return Integer.MAX_VALUE; + } + + @Override + public void afterRefresh() { + int size = instanceMap.size(); + instanceMap.clear(); + log.debug("缓存信息清理 => {} 条", size); + } + + /** + * 交由子类去加载实例信息 + * @return 实例信息 + */ + protected abstract Set load(); +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/DatabaseInstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/DatabaseInstanceFactory.java new file mode 100644 index 0000000..1f47e24 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/DatabaseInstanceFactory.java @@ -0,0 +1,19 @@ +package com.serliunx.ddns.core.factory; + +import com.serliunx.ddns.core.instance.Instance; + +import java.util.Collections; +import java.util.Set; + +/** + * 数据库实例工厂 + * @author SerLiunx + * @since 1.0 + */ +public abstract class DatabaseInstanceFactory extends AbstractInstanceFactory{ + + @Override + protected Set load() { + return Collections.emptySet(); + } +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/FileInstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/FileInstanceFactory.java new file mode 100644 index 0000000..bf19724 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/FileInstanceFactory.java @@ -0,0 +1,76 @@ +package com.serliunx.ddns.core.factory; + +import com.serliunx.ddns.core.InstanceFileFilter; +import com.serliunx.ddns.core.instance.Instance; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author SerLiunx + * @since 1.0 + */ +public abstract class FileInstanceFactory extends AbstractInstanceFactory { + + /** + * 存储实例信息的文件夹路径 + */ + protected String instanceDir; + + public FileInstanceFactory(String instanceDir) { + this.instanceDir = instanceDir; + } + + @Override + protected Set load() { + Set files = loadFiles(); + if(files != null && !files.isEmpty()){ + return files.stream() + .map(this::loadInstance) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(HashSet::new)); + } + return Collections.emptySet(); + } + + @Override + public int getPriority() { + return 256; + } + + /** + * 交由具体的子类去加载实例, 比如: json格式的实例信息、xml格式的实例信息 + * @param file 文件信息 + * @return 实例 + */ + protected abstract Instance loadInstance(File file); + + /** + * 子类要设置自己可以加载的文件后缀名 + *
  • 后缀名仅仅是一个标记符, 文件不一定要有后缀名哦 + * @return 文件后缀名 + */ + protected abstract String[] fileSuffix(); + + /** + * 载入目录下所有符合条件的文件 + */ + private Set loadFiles(){ + File pathFile = new File(instanceDir); + if(!pathFile.exists()){ + boolean result = pathFile.mkdirs(); + if(!result){ + throw new IllegalArgumentException("create path failed"); + } + } + if(!pathFile.isDirectory()){ + throw new IllegalArgumentException("path is not a directory"); + } + File[] files = pathFile.listFiles(new InstanceFileFilter(fileSuffix())); + if(files == null || files.length == 0){ + return Collections.emptySet(); + } + return Arrays.stream(files).collect(Collectors.toCollection(HashSet::new)); + } +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/InstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/InstanceFactory.java new file mode 100644 index 0000000..0e255f4 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/InstanceFactory.java @@ -0,0 +1,44 @@ +package com.serliunx.ddns.core.factory; + +import com.serliunx.ddns.core.Priority; +import com.serliunx.ddns.core.instance.Instance; +import com.serliunx.ddns.support.Refreshable; + +/** + * @author SerLiunx + * @since 1.0 + */ +public interface InstanceFactory extends Priority, Comparable, Refreshable { + + /** + * 添加实例 + *
  • 此方法默认为不覆盖的方式添加, 即如果存在则添加失败, 没有任何返回值和异常. + * @param instance 实例信息 + */ + void addInstance(Instance instance); + + /** + * 添加实例 + * @param instance 实例信息 + * @param override 是否覆盖原有的同名实例 + * @return 成功添加返回真, 否则返回假 + */ + boolean addInstance(Instance instance, boolean override); + + /** + * 根据实例名称获取实例 + * @param instanceName 实例名称 + * @return 实例信息, 如果不存在则会抛出异常 + */ + Instance getInstance(String instanceName); + + @Override + default int compareTo(InstanceFactory o) { + if(getPriority() < o.getPriority()){ + return 1; + } else if (this.getPriority() > o.getPriority()) { + return -1; + } + return 0; + } +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/JacksonFileInstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/JacksonFileInstanceFactory.java new file mode 100644 index 0000000..584df84 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/JacksonFileInstanceFactory.java @@ -0,0 +1,49 @@ +package com.serliunx.ddns.core.factory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.serliunx.ddns.constant.InstanceType; +import com.serliunx.ddns.constant.SystemConstants; +import com.serliunx.ddns.core.instance.Instance; + +import java.io.File; + +import static com.serliunx.ddns.constant.InstanceClasses.match; + +/** + * Jackson文件实例工厂, 使用jackson的ObjectMapper来分别处理json和xml + * @author SerLiunx + * @since 1.0 + * @see ObjectMapper + * @see com.fasterxml.jackson.dataformat.xml.XmlMapper + * @see com.fasterxml.jackson.databind.json.JsonMapper + */ +public abstract class JacksonFileInstanceFactory extends FileInstanceFactory{ + + private final ObjectMapper objectMapper; + + public JacksonFileInstanceFactory(String instanceDir, ObjectMapper objectMapper) { + super(instanceDir); + this.objectMapper = objectMapper; + } + + @Override + protected Instance loadInstance(File file) { + try{ + JsonNode root = objectMapper.readTree(file); + String rootName = root.get(SystemConstants.TYPE_FIELD).asText(); //根据类型去装配实例信息 + InstanceType instanceType = InstanceType.valueOf(rootName); + return post(objectMapper.treeToValue(root, match(instanceType))); + }catch (Exception e){ + throw new RuntimeException(e); + } + } + + @Override + protected abstract String[] fileSuffix(); + + /** + * 处理后续逻辑 + */ + protected abstract Instance post(Instance instance); +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/JsonFileInstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/JsonFileInstanceFactory.java new file mode 100644 index 0000000..8f748d0 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/JsonFileInstanceFactory.java @@ -0,0 +1,37 @@ +package com.serliunx.ddns.core.factory; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.serliunx.ddns.constant.InstanceSource; +import com.serliunx.ddns.core.instance.Instance; + +/** + * Jackson-Json文件实例工厂 + * @author SerLiunx + * @since 1.0 + */ +public class JsonFileInstanceFactory extends JacksonFileInstanceFactory{ + + public JsonFileInstanceFactory(String instanceDir, JsonMapper jsonMapper) { + super(instanceDir, jsonMapper); + } + + public JsonFileInstanceFactory(String instanceDir) { + this(instanceDir, new JsonMapper()); + } + + @Override + public int getPriority() { + return 1; + } + + @Override + protected String[] fileSuffix() { + return new String[]{".json"}; + } + + @Override + protected Instance post(Instance instance) { + instance.setSource(InstanceSource.FILE_JSON); + return instance; + } +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/ListableInstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/ListableInstanceFactory.java new file mode 100644 index 0000000..928addb --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/ListableInstanceFactory.java @@ -0,0 +1,36 @@ +package com.serliunx.ddns.core.factory; + +import com.serliunx.ddns.constant.InstanceType; +import com.serliunx.ddns.core.instance.Instance; + +import java.util.Map; +import java.util.Set; + +/** + * @author SerLiunx + * @since 1.0 + */ +public interface ListableInstanceFactory extends InstanceFactory { + + /** + * 获取所有已加载的实例信息 + * @return 所有实例信息 + */ + Set getInstances(); + + /** + * 获取指定类型的实例 + * @param type 类型 + * @return 实例名称-实例信息 键值对. + */ + Map getInstanceOfType(InstanceType type); + + /** + * 获取指定类型的实例 + * @param type 类型名称 + * @return 实例名称-实例信息 键值对. + */ + default Map getInstanceOfType(String type) { + return getInstanceOfType(InstanceType.valueOf(type)); + } +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/XmlFileInstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/XmlFileInstanceFactory.java new file mode 100644 index 0000000..ff35808 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/XmlFileInstanceFactory.java @@ -0,0 +1,37 @@ +package com.serliunx.ddns.core.factory; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.serliunx.ddns.constant.InstanceSource; +import com.serliunx.ddns.core.instance.Instance; + +/** + * Jackson-Xml文件实例工厂 + * @author SerLiunx + * @since 1.0 + */ +public class XmlFileInstanceFactory extends JacksonFileInstanceFactory{ + + public XmlFileInstanceFactory(String instanceDir, XmlMapper xmlMapper) { + super(instanceDir, xmlMapper); + } + + public XmlFileInstanceFactory(String instanceDir) { + this(instanceDir, new XmlMapper()); + } + + @Override + public int getPriority() { + return 2; + } + + @Override + protected String[] fileSuffix() { + return new String[]{".xml"}; + } + + @Override + protected Instance post(Instance instance) { + instance.setSource(InstanceSource.FILE_XML); + return instance; + } +} diff --git a/src/main/java/com/serliunx/ddns/core/factory/YamlFileInstanceFactory.java b/src/main/java/com/serliunx/ddns/core/factory/YamlFileInstanceFactory.java new file mode 100644 index 0000000..9069ea5 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/factory/YamlFileInstanceFactory.java @@ -0,0 +1,104 @@ +package com.serliunx.ddns.core.factory; + +import com.serliunx.ddns.constant.InstanceClasses; +import com.serliunx.ddns.constant.InstanceSource; +import com.serliunx.ddns.constant.InstanceType; +import com.serliunx.ddns.core.instance.Instance; +import com.serliunx.ddns.util.ReflectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Map; + +import static com.serliunx.ddns.constant.SystemConstants.TYPE_FIELD; + +/** + * @author SerLiunx + * @since 1.0 + */ +public class YamlFileInstanceFactory extends FileInstanceFactory { + + private static final Logger logger = LoggerFactory.getLogger(YamlFileInstanceFactory.class); + + public YamlFileInstanceFactory(String instanceDir) { + super(instanceDir); + } + + @Override + public int getPriority() { + return 3; + } + + @Override + protected Instance loadInstance(File file) { + FileInputStream instanceInputStream = null; + try { + instanceInputStream = new FileInputStream(file); + Yaml yaml = new Yaml(); + Map valueMap = yaml.load(instanceInputStream); + InstanceType type = null; + if (valueMap.get(TYPE_FIELD) != null) { + type = InstanceType.valueOf((String) valueMap.get(TYPE_FIELD)); + } + if (type == null) { + logger.error("文件 {} 读取失败, 可能是缺少关键参数.", file.getName()); + return null; + } + Class clazz = InstanceClasses.match(type); + if (clazz != null) { + Constructor constructor = clazz.getConstructor(); + Instance instance = buildInstance(constructor.newInstance(), valueMap); + instance.setSource(InstanceSource.FILE_YML); + return instance; + } + return null; + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + try { + if (instanceInputStream != null) { + instanceInputStream.close(); + } + } catch (IOException e) { + logger.error("文件读取出现异常."); + } + } + } + + @Override + protected String[] fileSuffix() { + return new String[]{".yml", ".yaml"}; + } + + @SuppressWarnings(value = {"unchecked", "rawtypes"}) + protected Instance buildInstance(Instance instance, Map valueMap){ + Field[] declaredFields = ReflectionUtils.getDeclaredFields(instance.getClass(), true); + for (Field f : declaredFields) { + if (Modifier.isStatic(f.getModifiers())) { + continue; + } + Object value = valueMap.get(f.getName()); + f.setAccessible(true); + try { + //设置枚举类 + Class clazz = f.getType(); + if (clazz.isEnum() && value != null) { + f.set(instance, Enum.valueOf((Class) clazz, (String) value)); + continue; + } + f.set(instance, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + f.setAccessible(false); + } + return instance; + } +} diff --git a/src/main/java/com/serliunx/ddns/core/instance/AbstractInstance.java b/src/main/java/com/serliunx/ddns/core/instance/AbstractInstance.java new file mode 100644 index 0000000..54aeb93 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/instance/AbstractInstance.java @@ -0,0 +1,137 @@ +package com.serliunx.ddns.core.instance; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.serliunx.ddns.constant.InstanceSource; +import com.serliunx.ddns.constant.InstanceType; + +import static com.serliunx.ddns.constant.SystemConstants.XML_ROOT_INSTANCE_NAME; + +/** + * @author SerLiunx + * @since 1.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JacksonXmlRootElement(localName = XML_ROOT_INSTANCE_NAME) +public abstract class AbstractInstance implements Instance { + + /** + * 实例名称 + *
  • 全局唯一 + */ + protected String name; + + /** + * 父实例名称 + */ + protected String fatherName; + + /** + * 执行周期 + */ + protected Long interval; + + /** + * 实例类型 + */ + protected InstanceType type; + + /** + * 实例来源 + */ + protected InstanceSource source; + + /** + * 获取到的ip地址. 仅做记录, 不需要手动设定 + */ + protected String value; + + @Override + public void refresh() { + // 调用子类的初始化逻辑 + init(); + } + + @Override + public void run() { + if(query()) + run0(); + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + public String getFatherName() { + return fatherName; + } + + public void setFatherName(String fatherName) { + this.fatherName = fatherName; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + @Override + public InstanceType getType() { + return type; + } + + @Override + public void setType(InstanceType instanceType) { + this.type = instanceType; + } + + @Override + public void setSource(InstanceSource instanceSource) { + this.source = instanceSource; + } + + public InstanceSource getSource() { + return source; + } + + @Override + public boolean validate() { + // 校验通用参数, 具体子类的参数交由子类校验 + if(name == null || name.isEmpty() || interval <= 0 || type == null){ + return false; + } + return validate0(); + } + + /** + * 具体的初始化逻辑 + */ + protected abstract void init(); + + /** + * 子类参数校验 + */ + protected abstract boolean validate0(); + + /** + * 更新前检查是否需要更新 + * @return 无需更新返回假, 否则返回真 + */ + protected abstract boolean query(); + + /** + * 具体执行逻辑 + */ + protected abstract void run0(); +} diff --git a/src/main/java/com/serliunx/ddns/core/instance/AliyunInstance.java b/src/main/java/com/serliunx/ddns/core/instance/AliyunInstance.java new file mode 100644 index 0000000..9d99b23 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/instance/AliyunInstance.java @@ -0,0 +1,227 @@ +package com.serliunx.ddns.core.instance; + +import com.aliyun.auth.credentials.Credential; +import com.aliyun.auth.credentials.provider.StaticCredentialProvider; +import com.aliyun.sdk.service.alidns20150109.AsyncClient; +import com.aliyun.sdk.service.alidns20150109.models.DescribeDomainRecordInfoRequest; +import com.aliyun.sdk.service.alidns20150109.models.DescribeDomainRecordInfoResponse; +import com.aliyun.sdk.service.alidns20150109.models.DescribeDomainRecordInfoResponseBody; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.serliunx.ddns.support.NetworkContextHolder; +import darabonba.core.client.ClientOverrideConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static com.serliunx.ddns.constant.SystemConstants.XML_ROOT_INSTANCE_NAME; + +/** + * 阿里云实例定义 + * @author SerLiunx + * @since 1.0 + */ +@SuppressWarnings("all") +@JacksonXmlRootElement(localName = XML_ROOT_INSTANCE_NAME) +public class AliyunInstance extends AbstractInstance { + + private static final Logger log = LoggerFactory.getLogger(AliyunInstance.class); + + /** + * AccessKey ID + */ + private String accessKeyId; + + /** + * AccessKey Secret + */ + private String accessKeySecret; + + /** + * 解析记录ID + */ + private String recordId; + + /** + * 主机记录。 + * 如果要解析@.example.com,主机记录要填写”@”,而不是空。 + * 示例值: + * www + */ + private String rr; + + /** + * 解析记录类型 + *
  • A记录 A 参考标准;RR值可为空,即@解析;不允许含有下划线; IPv4地址格式 + *
  • NS记录 NS 参考标准;RR值不能为空;允许含有下划线;不支持泛解析 NameType形式 + *
  • MX记录 MX 参考标准;RR值可为空,即@解析;不允许含有下划线 NameType形式,且不可为IP地址。1-10,优先级依次递减。 + *
  • TXT记录 TXT 参考标准;另外,有效字符除字母、数字、“-”(中横杠)、还包括“_”(下划线);RR值可为空,即@解析;允许含有下划线; + * 不支持泛解析 字符串;长度小于512,合法字符:大小写字母,数字,空格,及以下字符:-~=:;/.@+^!* + *
  • CNAME记录 CNAME 参考标准;另外,有效字符除字母、数字、“-”(中横杠)、还包括“_”(下划线);RR值不允许为空(即@); + * 允许含有下划线 NameType形式,且不可为IP + *
  • SRV记录 SRV 是一个name,且可含有下划线“_“和点“.”;允许含有下划线;可为空(即@);不支持泛解析 priority:优先级, + * 为0-65535之间的数字;weight:权重,为0-65535之间的数字;port:提供服务的端口号,为0-65535之间的数字 target:为提供服务的目标地址, + * 为nameType,且存在。参考:... + * ... + *
  • AAAA记录 AAAA 参考标准;RR值可为空,即@解析;不允许含有下划线; IPv6地址格式 + *
  • CAA记录 CAA 参考标准;RR值可为空,即@解析;不允许含有下划线; 格式为:[flag] [tag] [value],是由一个标志字节的[flag], + * 和一个被称为属性的标签[tag]-值[value]对组成。例如:@ 0 issue "symantec.com"或@ 0 iodef "mailto:admin@aliyun.com" + *
  • 显性URL转发 REDIRECT_URL 参考标准;RR值可为空,即@解析 NameType或URL地址(区分大小写),长度最长为500字符, + * 其中域名,如example.com,必须,大小写不敏感;协议:可选,如HTTP、HTTPS,默认为HTTP端口:可选,如81,默认为80;路径:可选,大小写敏感, + * 如/path/to/,默认为/;文件名:可选,大小写敏感,如file.php,默认无;参数:可选,大小写敏感,如?user=my***,默认无。 + *
  • 隐性URL转发 FORWARD_URL 参考标准;RR值可为空,即@解析 NameType或URL地址(区分大小写),长度最长为500字符,其中域名, + * 如example.com,必须,大小写不敏感;协议:可选,如HTTP、HTTPS,默认为HTTP端口:可选,如81,默认为80;路径:可选,大小写敏感, + * 如/path/to/,默认为/;文件名:可选,大小写敏感,如file.php,默认无;参数:可选,大小写敏感,如?user=my***,默认无。 + */ + private String recordType; + + @JsonIgnore + private AsyncClient client; + + @JsonIgnore + private JsonMapper jsonMapper; + + @Override + protected void init() { + jsonMapper = new JsonMapper(); + + StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder() + .accessKeyId(accessKeyId) + .accessKeySecret(accessKeySecret) + .build()); + client = AsyncClient.builder() + .region("cn-hangzhou") + .credentialsProvider(provider) + .overrideConfiguration( + ClientOverrideConfiguration.create() + .setEndpointOverride("alidns.cn-hangzhou.aliyuncs.com") + ) + .build(); + debug("初始化完成."); + } + + @Override + protected void run0() { + log("test"); + } + + @Override + protected boolean query() { + debug("正在校验是否需要更新记录."); + DescribeDomainRecordInfoRequest describeDomainRecordInfoRequest = DescribeDomainRecordInfoRequest.builder() + .recordId(recordId) + .build(); + CompletableFuture responseCompletableFuture = + client.describeDomainRecordInfo(describeDomainRecordInfoRequest); + try { + DescribeDomainRecordInfoResponse response = responseCompletableFuture.get(5, TimeUnit.SECONDS); + DescribeDomainRecordInfoResponseBody body = response.getBody(); + if(body != null){ + String recordValue = body.getValue(); + String ipAddress = NetworkContextHolder.getIpAddress(); + debug("当前记录值 => {}", recordValue); + boolean result = !(recordValue != null && !recordValue.isEmpty() + && recordValue.equals(ipAddress)); + if(result) + debug("需要更新IP地址: {} => {}", recordValue, ipAddress); + else + debug("无需更新."); + return result; + } + return false; + } catch (InterruptedException | ExecutionException e) { + error("出现了不应该出现的异常 => {}", e); + return false; + } catch (TimeoutException e) { + error("记录查询超时! 将跳过查询直接执行更新操作."); + return true; + } + } + + @Override + protected boolean validate0() { + //简单的必填参数校验 + return accessKeyId != null && !accessKeyId.isEmpty() && accessKeySecret != null && !accessKeySecret.isEmpty() + && recordId != null && !recordId.isEmpty() && rr != null && !rr.isEmpty() && recordType != null; + } + + public String getAccessKeyId() { + return accessKeyId; + } + + public void setAccessKeyId(String accessKeyId) { + this.accessKeyId = accessKeyId; + } + + public String getAccessKeySecret() { + return accessKeySecret; + } + + public void setAccessKeySecret(String accessKeySecret) { + this.accessKeySecret = accessKeySecret; + } + + public String getRecordId() { + return recordId; + } + + public void setRecordId(String recordId) { + this.recordId = recordId; + } + + public String getRr() { + return rr; + } + + public void setRr(String rr) { + this.rr = rr; + } + + public String getRecordType() { + return recordType; + } + + public void setRecordType(String recordType) { + this.recordType = recordType; + } + + public AsyncClient getClient() { + return client; + } + + public void setClient(AsyncClient client) { + this.client = client; + } + + public JsonMapper getJsonMapper() { + return jsonMapper; + } + + public void setJsonMapper(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; + } + + private void handleThrowable(Throwable t){ + error("出现异常 {}:", t.getCause(), t.getMessage()); + } + + @SuppressWarnings("all") + private void log(String msg, Object...params){ + log.info("[实例活动][" + name + "]" + msg, params); + } + + @SuppressWarnings("all") + private void debug(String msg, Object...params){ + log.debug("[实例活动][" + name + "]" + msg, params); + } + + @SuppressWarnings("all") + private void error(String msg, Object...params){ + log.error("[实例异常][" + name + "]" + msg, params); + } +} diff --git a/src/main/java/com/serliunx/ddns/core/instance/Instance.java b/src/main/java/com/serliunx/ddns/core/instance/Instance.java new file mode 100644 index 0000000..552c614 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/instance/Instance.java @@ -0,0 +1,78 @@ +package com.serliunx.ddns.core.instance; + +import com.serliunx.ddns.constant.InstanceSource; +import com.serliunx.ddns.constant.InstanceType; +import com.serliunx.ddns.support.Refreshable; + +/** + * @author SerLiunx + * @since 1.0 + */ +public interface Instance extends Runnable, Refreshable { + + /** + * 获取实例名称 + * @return 实例名称 + */ + String getName(); + + /** + * 设置实例名称 + * @param name 实例名称 + */ + void setName(String name); + + /** + * 获取父实例名称 + * @return 父实例名称 + */ + String getFatherName(); + + /** + * 设置父实例名称 + * @param fatherName 父实例名称 + */ + void setFatherName(String fatherName); + + /** + * 获取实例执行周期 (单位秒) + * @return 执行周期 + */ + Long getInterval(); + + /** + * 设置实例执行周期 (单位秒) + * @param interval 执行周期 + */ + void setInterval(Long interval); + + /** + * 获取实例类型 + * @return 实例类型 + */ + InstanceType getType(); + + /** + * 设置实例类型 + * @param instanceType 实例类型 + */ + void setType(InstanceType instanceType); + + /** + * 获取实例来源 + * @return 实例来源 + */ + InstanceSource getSource(); + + /** + * 设置实例来源 + * @param instanceSource 实例来源 + */ + void setSource(InstanceSource instanceSource); + + /** + * 实例参数校验 + * @return 通过校验返回真, 否则返回假 + */ + boolean validate(); +} diff --git a/src/main/java/com/serliunx/ddns/core/instance/TencentInstance.java b/src/main/java/com/serliunx/ddns/core/instance/TencentInstance.java new file mode 100644 index 0000000..eb2fdb3 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/core/instance/TencentInstance.java @@ -0,0 +1,38 @@ +package com.serliunx.ddns.core.instance; + +/** + * @author SerLiunx + * @since 1.0 + */ +public class TencentInstance extends AbstractInstance { + + @Override + protected void init() { + + } + + @Override + protected void run0() { + + } + + @Override + protected boolean query() { + return false; + } + + @Override + protected boolean validate0() { + return false; + } + + @Override + public String toString() { + return "TencentInstance{" + + "source=" + source + + ", type=" + type + + ", fatherName='" + fatherName + '\'' + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/main/java/com/serliunx/ddns/support/Assert.java b/src/main/java/com/serliunx/ddns/support/Assert.java new file mode 100644 index 0000000..2ac2dcd --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/Assert.java @@ -0,0 +1,52 @@ +package com.serliunx.ddns.support; + +import java.util.Collection; + +/** + * 断言 + * @author SerLiunx + * @since 1.0 + */ +public final class Assert { + + private Assert(){throw new UnsupportedOperationException();} + + public static void notNull(Object object){ + notNull(object, null); + } + + public static void notNull(Object object, String msg){ + if(object == null) + throw new NullPointerException(msg); + } + + public static void notNull(Object...objects){ + for (Object object : objects) { + notNull(object); + } + } + + public static void isPositive(int i){ + if(i <= 0){ + throw new IllegalArgumentException("指定参数必须大于0!"); + } + } + + public static void isLargerThan(int source, int target){ + if(source <= target){ + throw new IllegalArgumentException(String.format("%s太小了, 它必须大于%s", source, target)); + } + } + + public static void notEmpty(Collection collection){ + notNull(collection); + if(collection.isEmpty()) + throw new IllegalArgumentException("参数不能为空!"); + } + + public static void notEmpty(CharSequence charSequence){ + notNull(charSequence); + if(charSequence.length() == 0) + throw new IllegalArgumentException("参数不能为空!"); + } +} diff --git a/src/main/java/com/serliunx/ddns/support/Configurer.java b/src/main/java/com/serliunx/ddns/support/Configurer.java new file mode 100644 index 0000000..743d76f --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/Configurer.java @@ -0,0 +1,33 @@ +package com.serliunx.ddns.support; + +import com.serliunx.ddns.config.Configuration; +import com.serliunx.ddns.core.context.MultipleSourceInstanceContext; + +/** + * @author SerLiunx + * @since 1.0 + */ +public final class Configurer { + + private Configuration configuration; + private MultipleSourceInstanceContext instanceContext; + + Configurer(){} + + public Configurer configuration(Configuration configuration){ + Assert.notNull(configuration); + this.configuration = configuration; + return this; + } + + public Configurer instanceContext(MultipleSourceInstanceContext instanceContext){ + Assert.notNull(instanceContext); + this.instanceContext = instanceContext; + return this; + } + + public SystemInitializer done(){ + Assert.notNull(configuration, instanceContext); + return new SystemInitializer(configuration, instanceContext); + } +} diff --git a/src/main/java/com/serliunx/ddns/support/NetworkContextHolder.java b/src/main/java/com/serliunx/ddns/support/NetworkContextHolder.java new file mode 100644 index 0000000..4125684 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/NetworkContextHolder.java @@ -0,0 +1,56 @@ +package com.serliunx.ddns.support; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 网络参数上下文, 目前仅用于存储本机网络IP + * @author SerLiunx + * @since 1.0 + */ +public final class NetworkContextHolder { + + private static final Logger log = LoggerFactory.getLogger(NetworkContextHolder.class); + private static final Lock IP_LOCK = new ReentrantLock(); + // 防止初始化未完成. + private static final CountDownLatch IP_CONTEXT_WAIT_LATCH = new CountDownLatch(1); + // 外网IP地址获取 + private static final Integer IP_CONTEXT_TIME_OUT = 5; + private static volatile String IP_ADDRESS; + + private NetworkContextHolder(){throw new UnsupportedOperationException();} + + public static void setIpAddress(String i){ + try { + IP_LOCK.lock(); + IP_ADDRESS = i; + if(IP_CONTEXT_WAIT_LATCH.getCount() > 0){ + IP_CONTEXT_WAIT_LATCH.countDown(); + } + }finally { + IP_LOCK.unlock(); + } + } + + public static String getIpAddress(){ + log.debug("正在尝试获取最新的IP地址."); + if(IP_ADDRESS != null) + return IP_ADDRESS; + try { + if(!IP_CONTEXT_WAIT_LATCH.await(IP_CONTEXT_TIME_OUT, TimeUnit.SECONDS)){ + log.error("IP地址获取超时."); + return null; + } + log.debug("最新的IP地址获取成功."); + return IP_ADDRESS; + } catch (InterruptedException e) { + log.error("IP地址获取出现异常 => {}", e.getMessage()); + } + return null; + } +} diff --git a/src/main/java/com/serliunx/ddns/support/Refreshable.java b/src/main/java/com/serliunx/ddns/support/Refreshable.java new file mode 100644 index 0000000..c6f44a8 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/Refreshable.java @@ -0,0 +1,22 @@ +package com.serliunx.ddns.support; + +/** + * 刷新逻辑 + * @author SerLiunx + * @since 1.0 + */ +@FunctionalInterface +public interface Refreshable { + + /** + * 刷新(初始化) + */ + void refresh(); + + /** + * 刷新后逻辑定义, 一般用于资源清理 + */ + default void afterRefresh(){ + + } +} diff --git a/src/main/java/com/serliunx/ddns/support/SystemInitializer.java b/src/main/java/com/serliunx/ddns/support/SystemInitializer.java new file mode 100644 index 0000000..b27bf5d --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/SystemInitializer.java @@ -0,0 +1,162 @@ +package com.serliunx.ddns.support; + +import com.serliunx.ddns.config.Configuration; +import com.serliunx.ddns.constant.SystemConstants; +import com.serliunx.ddns.core.context.MultipleSourceInstanceContext; +import com.serliunx.ddns.core.instance.Instance; +import com.serliunx.ddns.support.feign.client.IPAddressClient; +import com.serliunx.ddns.support.feign.client.entity.IPAddressResponse; +import com.serliunx.ddns.thread.TaskThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static com.serliunx.ddns.config.ConfigurationKeys.KEY_TASK_REFRESH_INTERVAL_IP; +import static com.serliunx.ddns.config.ConfigurationKeys.KEY_THREAD_POOL_CORE_SIZE; + +/** + * 系统初始化 + * @author SerLiunx + * @since 1.0 + */ +public final class SystemInitializer implements Refreshable{ + + private static final Logger log = LoggerFactory.getLogger(SystemInitializer.class); + + private final Configuration configuration; + private final MultipleSourceInstanceContext instanceContext; + + private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor; + private Set instances; + + SystemInitializer(Configuration configuration, MultipleSourceInstanceContext instanceContext) { + this.configuration = configuration; + this.instanceContext = instanceContext; + } + + public static Configurer configurer(){ + return new Configurer(); + } + + @Override + public void refresh() { + log.info("程序正在初始化, 请稍候."); + + // 释放配置文件 + releaseResource(SystemConstants.PROPERTIES_FILE); + + // 刷新配置信息 + configuration.refresh(); + + // 获取核心线程数量, 默认为CPU核心数量 + int coreSize = configuration.getInteger(KEY_THREAD_POOL_CORE_SIZE, Runtime.getRuntime().availableProcessors()); + + // 初始化线程池 + initThreadPool(coreSize); + + // 加载实例(不同的容器加载时机不同) + loadInstances(); + + // 运行实例 + runInstances(); + log.info("初始化完成!"); + } + + public MultipleSourceInstanceContext getInstanceContext() { + return instanceContext; + } + + public Set getInstances() { + return instances; + } + + @Override + public void afterRefresh() { + // TODO + } + + private void loadInstances() { + instances = instanceContext.getInstances(); + log.info("载入 {} 个实例.", instances.size()); + } + + private void releaseResource(String resourceName){ + ClassLoader classLoader = SystemConstants.class.getClassLoader(); + Path path = Paths.get(SystemConstants.USER_DIR + File.separator + resourceName); + // 检查文件是否已存在 + if(Files.exists(path)){ + log.debug("文件 {} 已存在, 无需解压.", resourceName); + return; + } + try (InputStream inputStream = classLoader.getResourceAsStream(resourceName)) { + log.debug("正在解压文件 {} 至路径: {}", resourceName, SystemConstants.USER_DIR); + // 创建输出流,写入文件到指定目录 + OutputStream outputStream = Files.newOutputStream(path); + byte[] buffer = new byte[1024]; + int bytesRead; + if(inputStream != null){ + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + outputStream.close(); + }catch (Exception e){ + log.error("文件 {} 解压失败!, 原因: {}", resourceName, e.getMessage()); + } + } + + private void runInstances() { + Assert.notNull(scheduledThreadPoolExecutor); + Assert.notNull(instances); + + for (Instance i : instances) { + if(!i.validate()){ + log.error("实例{}({})参数校验不通过, 将不会被运行.", i.getName(), i.getType()); + continue; + } + // 初始化实例 + i.refresh(); + scheduledThreadPoolExecutor.scheduleWithFixedDelay(i, 0, i.getInterval(), TimeUnit.SECONDS); + log.info("{}({})已启动, 运行周期 {} 秒.", i.getName(), i.getType(), i.getInterval()); + } + } + + private void initThreadPool(int coreSize){ + Assert.isLargerThan(coreSize, 1); + scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(coreSize, new TaskThreadFactory()); + + // 初始化一个线程保活 + scheduledThreadPoolExecutor.submit(() -> {}); + + // 提交定时获取网络IP的定时任务 + scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> { + log.info("正在尝试获取本机最新的IP地址."); + IPAddressResponse response = IPAddressClient.instance.getIPAddress(); + String ip; + if(response != null + && (ip = response.getQuery()) != null){ + NetworkContextHolder.setIpAddress(ip); + log.info("本机最新公网IP地址 => {}", ip); + } + }, 0, configuration.getLong(KEY_TASK_REFRESH_INTERVAL_IP, 300L), TimeUnit.SECONDS); + + // 添加进程结束钩子函数 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + MDC.put("pid", SystemSupport.getPid()); + log.info("程序正在关闭中, 可能需要一定时间."); + afterRefresh(); + scheduledThreadPoolExecutor.shutdown(); + log.info("已关闭."); + }, "DDNS-ShutDownHook")); + } +} diff --git a/src/main/java/com/serliunx/ddns/support/SystemSupport.java b/src/main/java/com/serliunx/ddns/support/SystemSupport.java new file mode 100644 index 0000000..1e8f301 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/SystemSupport.java @@ -0,0 +1,22 @@ +package com.serliunx.ddns.support; + +import java.lang.management.ManagementFactory; + +/** + * @author SerLiunx + * @since 1.0 + */ +public final class SystemSupport { + + private static final String PID; + + static{ + PID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; + } + + private SystemSupport(){throw new UnsupportedOperationException();} + + public static String getPid(){ + return PID; + } +} diff --git a/src/main/java/com/serliunx/ddns/support/feign/JacksonDecoder.java b/src/main/java/com/serliunx/ddns/support/feign/JacksonDecoder.java new file mode 100644 index 0000000..4560e95 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/feign/JacksonDecoder.java @@ -0,0 +1,72 @@ +package com.serliunx.ddns.support.feign; + +import com.fasterxml.jackson.databind.*; +import feign.FeignException; +import feign.Response; +import feign.Util; +import feign.codec.Decoder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; +import java.util.Collections; + +/** + * feign解码器 + * @author SerLiunx + * @since 1.0 + */ +public class JacksonDecoder implements Decoder { + + private final ObjectMapper mapper; + private static final JacksonDecoder decoder = new JacksonDecoder(); + + private JacksonDecoder() { + this(Collections.emptyList()); + } + + private JacksonDecoder(Iterable modules) { + this(new ObjectMapper() + //设置下划线自动转化为驼峰命名 + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModules(modules)); + } + + private JacksonDecoder(ObjectMapper mapper) { + this.mapper = mapper; + } + + public static Decoder getInstance(){ + return decoder; + } + + @Override + public Object decode(Response response, Type type) throws FeignException, IOException { + if (response.status() == 404 || response.status() == 204) + return Util.emptyValueOf(type); + if (response.body() == null) + return null; + Reader reader = response.body().asReader(response.charset()); + if (!reader.markSupported()) { + reader = new BufferedReader(reader, 1); + } + //处理响应体字符流 + try{ + reader.mark(1); + if (reader.read() == -1) { + return null; + } + reader.reset(); + return mapper.readValue(reader, mapper.constructType(type)); + } catch (RuntimeJsonMappingException e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } + throw e; + }finally { + response.close(); + } + } +} diff --git a/src/main/java/com/serliunx/ddns/support/feign/JacksonEncoder.java b/src/main/java/com/serliunx/ddns/support/feign/JacksonEncoder.java new file mode 100644 index 0000000..a66a035 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/feign/JacksonEncoder.java @@ -0,0 +1,55 @@ +package com.serliunx.ddns.support.feign; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import feign.RequestTemplate; +import feign.Util; +import feign.codec.EncodeException; +import feign.codec.Encoder; + +import java.lang.reflect.Type; +import java.util.Collections; + +/** + * Feign兼容Jackson(反序列化返回值) + * @author SerLiunx + * @since 1.0 + */ +public class JacksonEncoder implements Encoder { + + private final ObjectMapper mapper; + private static final JacksonEncoder encoder = new JacksonEncoder(); + + private JacksonEncoder() { + this(Collections.emptyList()); + } + + private JacksonEncoder(Iterable modules) { + this(new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(SerializationFeature.INDENT_OUTPUT, true) + .registerModules(modules)); + } + + private JacksonEncoder(ObjectMapper mapper) { + this.mapper = mapper; + } + + public static Encoder getInstance(){ + return encoder; + } + + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { + try { + JavaType javaType = mapper.getTypeFactory().constructType(bodyType); + template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8); + } catch (JsonProcessingException e) { + throw new EncodeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/serliunx/ddns/support/feign/client/IPAddressClient.java b/src/main/java/com/serliunx/ddns/support/feign/client/IPAddressClient.java new file mode 100644 index 0000000..ea54480 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/feign/client/IPAddressClient.java @@ -0,0 +1,40 @@ +package com.serliunx.ddns.support.feign.client; + +import com.serliunx.ddns.support.feign.JacksonDecoder; +import com.serliunx.ddns.support.feign.JacksonEncoder; +import com.serliunx.ddns.support.feign.client.entity.IPAddressResponse; +import feign.Feign; +import feign.Request; +import feign.RequestLine; + +import java.util.concurrent.TimeUnit; + +/** + * 本机外网IP地址获取 + * @author SerLiunx + * @since 1.0 + */ +@SuppressWarnings("all") +public interface IPAddressClient { + + static final String url = "http://ip-api.com"; + + static final IPAddressClient instance = getInstance(); + + /** + * 获取本机外网IP地址 + * @return IPAddressResponse + */ + @RequestLine("GET /json") + IPAddressResponse getIPAddress(); + + static IPAddressClient getInstance(){ + return Feign.builder() + .encoder(JacksonEncoder.getInstance()) + .decoder(JacksonDecoder.getInstance()) + .options(new Request.Options(10, + TimeUnit.SECONDS, 10, + TimeUnit.SECONDS, true)) + .target(IPAddressClient.class, url); + } +} diff --git a/src/main/java/com/serliunx/ddns/support/feign/client/entity/IPAddressResponse.java b/src/main/java/com/serliunx/ddns/support/feign/client/entity/IPAddressResponse.java new file mode 100644 index 0000000..09b11c9 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/support/feign/client/entity/IPAddressResponse.java @@ -0,0 +1,156 @@ +package com.serliunx.ddns.support.feign.client.entity; + +/** + * IP地址查询响应 + * @author SerLiunx + * @since 1.0 + */ +@SuppressWarnings("all") +public class IPAddressResponse { + private String query; + private String status; + private String country; + private String countryCode; + private String region; + private String regionName; + private String city; + private String zip; + private String lat; + private String lon; + private String timezone; + private String isp; + private String org; + private String as; + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCountryCode() { + return countryCode; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public String getRegionName() { + return regionName; + } + + public void setRegionName(String regionName) { + this.regionName = regionName; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getZip() { + return zip; + } + + public void setZip(String zip) { + this.zip = zip; + } + + public String getLat() { + return lat; + } + + public void setLat(String lat) { + this.lat = lat; + } + + public String getLon() { + return lon; + } + + public void setLon(String lon) { + this.lon = lon; + } + + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + public String getIsp() { + return isp; + } + + public void setIsp(String isp) { + this.isp = isp; + } + + public String getOrg() { + return org; + } + + public void setOrg(String org) { + this.org = org; + } + + public String getAs() { + return as; + } + + public void setAs(String as) { + this.as = as; + } + + @Override + public String toString() { + return "IPAddressResponse{" + + "as='" + as + '\'' + + ", org='" + org + '\'' + + ", isp='" + isp + '\'' + + ", timezone='" + timezone + '\'' + + ", lon='" + lon + '\'' + + ", lat='" + lat + '\'' + + ", zip='" + zip + '\'' + + ", city='" + city + '\'' + + ", regionName='" + regionName + '\'' + + ", region='" + region + '\'' + + ", countryCode='" + countryCode + '\'' + + ", country='" + country + '\'' + + ", status='" + status + '\'' + + ", query='" + query + '\'' + + '}'; + } +} diff --git a/src/main/java/com/serliunx/ddns/thread/TaskThreadFactory.java b/src/main/java/com/serliunx/ddns/thread/TaskThreadFactory.java new file mode 100644 index 0000000..c0001ef --- /dev/null +++ b/src/main/java/com/serliunx/ddns/thread/TaskThreadFactory.java @@ -0,0 +1,28 @@ +package com.serliunx.ddns.thread; + +import com.serliunx.ddns.support.Assert; +import com.serliunx.ddns.support.SystemSupport; +import org.jetbrains.annotations.NotNull; +import org.slf4j.MDC; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author SerLiunx + * @since 1.0 + */ +public class TaskThreadFactory implements ThreadFactory { + + private final AtomicInteger count = new AtomicInteger(0); + + @Override + public Thread newThread(@NotNull Runnable r) { + Assert.notNull(r); + Runnable runnable = () -> { + MDC.put("pid", SystemSupport.getPid()); + r.run(); + }; + return new Thread(runnable, String.format("ddns-task-%s", count.getAndIncrement())); + } +} diff --git a/src/main/java/com/serliunx/ddns/util/InstanceUtils.java b/src/main/java/com/serliunx/ddns/util/InstanceUtils.java new file mode 100644 index 0000000..cd62143 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/util/InstanceUtils.java @@ -0,0 +1,22 @@ +package com.serliunx.ddns.util; + +import com.serliunx.ddns.core.instance.Instance; +import com.serliunx.ddns.support.Assert; + +/** + * 实例相关工具方法集合 + * @author SerLiunx + * @since 1.0 + */ +public final class InstanceUtils { + + private InstanceUtils(){throw new UnsupportedOperationException();} + + public static void validateInstance(Instance instance){ + Assert.notNull(instance); + String instanceName = instance.getName(); + if(instanceName == null || instanceName.isEmpty()){ + throw new NullPointerException(); + } + } +} diff --git a/src/main/java/com/serliunx/ddns/util/ReflectionUtils.java b/src/main/java/com/serliunx/ddns/util/ReflectionUtils.java new file mode 100644 index 0000000..5a45e34 --- /dev/null +++ b/src/main/java/com/serliunx/ddns/util/ReflectionUtils.java @@ -0,0 +1,86 @@ +package com.serliunx.ddns.util; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +/** + * 反射相关工具类 + * @author SerLiunx + * @since 1.0 + */ +public final class ReflectionUtils { + + private ReflectionUtils(){throw new UnsupportedOperationException();} + + /** + * 获取当前类声明的所有字段 + *
  • 包括父类 + * @param clazz 类对象 + * @param setAccessible 是否将字段的可访问性 + * @return 字段列表 + */ + public static Field[] getDeclaredFields(Class clazz, boolean setAccessible){ + if(clazz == null){ + return null; + } + Field[] declaredFields = clazz.getDeclaredFields(); + Field[] declaredFieldsInSuper = getDeclaredFields(clazz.getSuperclass(), setAccessible); + if(declaredFieldsInSuper != null){ + Field[] newFields = new Field[declaredFields.length + declaredFieldsInSuper.length]; + System.arraycopy(declaredFields, 0, newFields, 0, declaredFields.length); + System.arraycopy(declaredFieldsInSuper, 0, newFields, declaredFields.length, declaredFieldsInSuper.length); + declaredFields = newFields; + } + if(setAccessible){ + for (Field declaredField : declaredFields) { + declaredField.setAccessible(true); + } + } + return declaredFields; + } + + /** + * 获取当前类声明的所有字段 + *
  • 包括父类 + * @param clazz 类对象 + * @param setAccessible 是否将字段的可访问性 + * @return 字段列表 + */ + public static List getDeclaredFieldList(Class clazz, boolean setAccessible){ + return Arrays.asList(getDeclaredFields(clazz, setAccessible)); + } + + /** + * 复制两个对象的同名属性 + * @param src 源对象 + * @param dest 目标对象 + * @param onlyNull 是否仅复制源对象不为空的属性 + */ + public static void copyField(Object src, Object dest,boolean onlyNull){ + Class srcClass = src.getClass(); + Class destClass = dest.getClass(); + List srcField = getDeclaredFieldList(srcClass, true); + List destField = getDeclaredFieldList(destClass, true); + for (Field field : destField) { + if(onlyNull){ + try { + if(field.get(dest) != null){ + continue; + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + for (Field sf : srcField) { + if(sf.getName().equals(field.getName())){ + try { + field.set(dest, sf.get(src)); + }catch (Exception e){ + throw new RuntimeException(e); + } + } + } + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..6318b0f --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + %d{yyyy年MM月dd日 HH:mm:ss(SSS)} [%X{pid}] [%-15thread] [%level] %logger{16}: %msg%n + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/settings.properties b/src/main/resources/settings.properties new file mode 100644 index 0000000..4a7ab61 --- /dev/null +++ b/src/main/resources/settings.properties @@ -0,0 +1,3 @@ +system.cfg.log.onstart=true +system.pool.core.size=16 +system.task.refresh.interval.ip=300 \ No newline at end of file diff --git a/src/test/java/com/serliunx/ddns/test/ContextTest.java b/src/test/java/com/serliunx/ddns/test/ContextTest.java new file mode 100644 index 0000000..de56467 --- /dev/null +++ b/src/test/java/com/serliunx/ddns/test/ContextTest.java @@ -0,0 +1,27 @@ +package com.serliunx.ddns.test; + +import com.serliunx.ddns.constant.SystemConstants; +import com.serliunx.ddns.core.context.GenericInstanceContext; +import com.serliunx.ddns.core.factory.JsonFileInstanceFactory; +import com.serliunx.ddns.core.factory.XmlFileInstanceFactory; +import com.serliunx.ddns.core.factory.YamlFileInstanceFactory; +import org.junit.Test; + +/** + * @author SerLiunx + * @since 1.0 + */ +public class ContextTest { + + @Test + public void testContext(){ + GenericInstanceContext genericInstanceContext = new GenericInstanceContext(); + + genericInstanceContext.addListableInstanceFactory(new XmlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR)); + genericInstanceContext.addListableInstanceFactory(new YamlFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR)); + genericInstanceContext.addListableInstanceFactory(new JsonFileInstanceFactory(SystemConstants.USER_INSTANCE_DIR)); + + genericInstanceContext.refresh(); + genericInstanceContext.getInstances().forEach(System.out::println); + } +}