文档章节

Migrating a SpringWebMVC App from JSP to AngularJS

m
 miscellanea
发布于 2015/08/24 17:56
字数 3669
阅读 209
收藏 2
点赞 0
评论 0

Migrating a Spring Web MVC application from JSP to AngularJS

August 19, 2015

Note on authors

This post is a guest post by Han Lim and Tony Nguyen. Han and Tony have done a great presentation at our Singapore Spring User Group on Spring + Angular JS. This blog is based on their presentation.

Abstractf

In this article, we try to describe our experiences moving from server-side rendering view technologies like JSP, Struts and Velocity to client-side rendering view technologies using AngularJS, a popular Javascript framework for modern browsers. We will talk about some of the things to look out for when you are making this change and potential pitfalls you may encounter. If you are experienced in Spring Web MVC and JSP development and would like to find out how Spring MVC can work together with a client-side Javascript like AngularJS, this article may just be for you.

There is also an appendix that gives some additional insights on AngularJS that may seem strange or unfamiliar to people coming from the JSP world.

Sample Petclinic for reference

We have created a fork of the Spring Petclinic application and experimented with converting it to AngularJS (with a new design courtesy of Andrew Abogado). Our fork can be found here.

Preparation

When you start to migrate from a server-side templating engine like JSP or Thymeleaf to a Javascript-based templating engine on the client side, you will need to adopt a paradigm shift towards a client-server architecture. You have to cease thinking of the view as being a part of the web application and instead conceive the web application as 2 separate client-side and server-side applications. The AngularJS application thus becomes an application on its own that runs on your web browser and it communicate with the backend services provided by Spring MVC. The only commonality between the the Spring MVC application and the AngularJS is probably going to be the fact that they are deployed in the same Java WAR file and that the index file is served out of a JSP.

An illustration of this is in the diagram below which shows how a Spring application becomes a provider of RESTful Web Services, servicing various front end applications including an AngularJS browser-based application as well as a possibility to provide services for mobile clients like tablets or smartphones. These services could include OAuth, Authentication and other business logic services which should be obfuscated from public view. One should bear in mind that any data or business logic that is published in the form of JSON or javascript files are exposed for the client-side to see. Thus, if there is any business sensitive logic or workflow that should not be exposed, it should only be performed on the backend.

Another difference to note about using AngularJS instead of JSP is that we would prefer not to use HTML forms and the traditional form submissions to pass data to the server side. Instead, we will prefer to encapsulate form submissions in a JSON object which is sent over to the backend RESTful service via a AngularJS HTTP Post method call. In fact, we will prefer to use full range of HTTP verbs that is encouraged in developing RESTful services.

If you need to perform validation on user inputs, it can be done on the front end using AngularJS’s built-in validation or your own custom input validation. You should always validate your data before posting it to the server. It is also prudent to validate the same data at the server side as well to ensure that clients that do not check their data do not compromise the integrity of the data on the server side.

Architecture

Application structure

Let us now discuss how you could organize your Spring + AngularJS application. At WDS (our company), we use Maven as our dependency and package management tool for Java/Spring and that influenced how we decided to place our AngularJS javascript application. The AngularJS application is created withinsrc/main/webappand the main files are

components/ # the various components are stored here. js/app.js # where we bootstrap the application plugins/ # additional external plugins e.g. jquery. services/ # common services are stored here. images/ videos/

You can see an image capture of the folder structure in Eclipse below.

folders

Resources here are organized per thefeature-groupingmethod. There are also ways to group your resources based on types, e.g. grouping all your controllers, services and views into its namesake folder. There are pros and cons for each of those options.

There are also some Javascript-based package managers like npm or bower that you might want to consider using to simplify the management of your external dependencies. If you are using bower, you will have a folder created called bower_components where all the dependency resources will be installed. You will then need to include them in your templates as you would do for any Javascript library. As for npm, you can use it to manage all your Javascript server side system tools like Grunt (a sort of Ant-like task runner)

Using AngularJS directives vs JSP custom tags

If you have used Spring’s custom form tags in your JSPs for developing your forms, you may be wondering if AngularJS provides the same kind of convenience for mapping form inputs to objects. The answer is yes! As a matter of fact, it is easy to bind any HTML element to a Javascript object. The only difference is that now the binding occurs on the client-side instead of the server-side.

<form:form method="POST" commandName="user"> <table> <tr> <td>User Name :</td> <td><form:input path="name" /></td> </tr> <tr> <td>Password :</td> <td><form:password path="password" /></td> </tr> <tr> <td>Country :</td> <td> <form:select path="country"> <form:option value="0" label="Select" /> <form:options items="${countryList}" itemValue="countryId" itemLabel="countryName" /> </form:select> </td> </tr> </table> </form:form>

Here is an example of the same form in AngularJS

<form name="UserForm" data-ng-controller="ExampleUserController"> <table> <tr> <td>User Name :</td> <td><input data-ng-model="user.name" /></td> </tr> <tr> <td>Password :</td> <td><input type="password" data-ng-model="user.password" /></td> </tr> <tr> <td>Country :</td> <td> <select data-ng-model="user.country" data-ng-options="country as country.label for country in countries"> <option value="">Select<option /> </select> </td> </tr> </table> </form>

Form inputs in AngularJS are augmented with additional capabilities like thengRequireddirective that makes the field mandatory based on certain conditions. There are also built-in validation for checking ranges, dates, patterns etc.. You can find out more at AngularJS’s official documentation found here which provides all the relevant form input directives.

Considerations when moving from JSP to AngularJS

In order to successfully migrate your JSP-based application to one that uses AngularJS, there are a few factors to consider.

Converting your Spring controllers to RESTful services

You will need to transform your controllers so instead of forwarding the response to a templating engine to render a view to the client, you will provide services that will be serialized into JSON data instead. The following is an example of how a standard Spring MVC controllerRequestMappinguses theModelAndViewobject to render a view with the Owner as described in the url mapping.

@RequestMapping("/api/owners/{ownerId}") public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) { ModelAndView mav = new ModelAndView("owners/ownerDetails"); mav.addObject(this.clinicService.findOwnerById(ownerId)); return mav; }

A controller RequestMapping like that can be converted into an equivalent RESTful service that returns the owner based on the ownerId. Your template can then be moved into AngularJS which will then bind the owner object to the AngularJS template.

@RequestMapping(value = "/api/owners/{id}", method = RequestMethod.GET) public @ResponseBody Owner find(@PathVariable Integer id) { return this.clinicService.findOwnerById(id); }

In order for Spring MVC to convert your returned object (which need to be Serializable) to a JSON object, you can use the Jackson2 serialization library which is part of the Spring MVC dependency. In the example below, we had to customize the date serialization format by Jackson2 so we added the xml snippet in our Spring Context xml file to describe the date format for our JSON ObjectMapper Factory so that it knows that the Jackson2 ObjectMapper requires a date of such format. You can see the snippet that performs this Spring context configuration below. If there’s no customization of the date format (or any other serialization requirements), you can use the default ones which means that you do not even need to include this section as Spring MVC by default will component scan the ObjectMapper and inject it into your controller class via autowiring.

<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean" p:indentOutput="true" p:simpleDateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSZ"></bean> <mvc:annotation-driven conversion-service="conversionService" > <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" > <property name="objectMapper" ref="objectMapper" /> </bean> </mvc:message-converters> </mvc:annotation-driven>

Once you have converted your controllers into RESTful services, you can then access these resources from your AngularJS application.

One nice way to access RESTful services in AngularJS is to use the built-inngResourcedirective that allows you to access your RESTful services in an elegant and concise manner. An example of the Javascript code to access RESTful services using this directive can be illustrated by the following:

var Owner = ['$resource','context', function($resource, context) { return $resource(context + '/api/owners/:id'); }]; app.factory('Owner', Owner); var OwnerController = ['$scope','$state','Owner',function($scope,$state,Owner) { $scope.$on('$viewContentLoaded', function(event){ $('html, body').animate({ scrollTop: $("#owners").offset().top }, 1000); }); $scope.owners = Owner.query(); }];

The above snippet shows how a “resource” can be created by declaring an Owner resource and then initialising it as an Owner service. The controller can then use this service to query for Owners from the RESTful endpoint. In this way, you can easily create the resources that your application require and map it easily to your business domain model. This declaration is done once only in the app.js file. You can actually take a look at this actual file in action here.

When moving to RestAPI, it is important to remember that the RestAPI is the public interface rather than the website content. The JSON model is fully visible to users.
For example, if we need to display user profiles, password masking should be done on the JSON object rather than in the template. In order to do this, sometimes we need to create DTO objects for our RestAPI.

Synchronizing states between the backend and your AngularJS application

Synchronizing states is something that needs to be managed when you are developing a client-server architecture. You will need to give some thought to how your application updates its state from the backend or refresh its view whenever some state changes.

Authentication

Having your client-side code exposed to the public makes it even more important to think through how you would like to authenticate your users and maintain a session with your application. One important consideration in deciding your authentication method is to choose between a stateful session or a stateless one depending on your application architecture.

You can check Dave Syer’s series of blogs on how to integrate AngularJS with Spring Security here.

Testing

AngularJS comes with the necessary tools to help you perform testing at all layers of your Javascript development from unit to functional testing. Planning how you test and perform builds incorporating those tests will determine the quality of your front end client. We use a maven plugin calledfrontend-maven-pluginto assist us in our build tests.

Conclusion

Migrating to AngularJS from JSP may seem daunting but it can be very rewarding in the long run as it makes for a more maintainable and testable user interface. The trend towards client side rendered views also encourages building more responsive web applications that were previously hampered by the design in server side rendering. The advent of HTML 5 and CSS3 has ushered us to a new era in View rendering technologies, with different competing frameworks like EmberJs, ReactJs, BackboneJs etc.. However, in terms of momentum, AngularJS has been getting a lot of attention and having used it for a while, we can see why. We hope that this article contain useful tips for the people who intend to take the plunge. You can examine the fork of the Spring Petclinic that has some code examples to see how we did it.

Appendix

A brief introduction to AngularJS

AngularJS is a Javascript framework created at Google that touts itself as a “Superheroic Web MVW Framework” (where the “W” in the “MVW” being a tongue-in-cheek reference to “Whatever” for all the various MVx architectures. As it is based on an MVx architecture, AngularJS provides a structure to Javascript development and thus gives Javascript an elevated status compared to traditional Spring + JSP applications that only use Javascript to provide that bit of interactivity on the user interface.

With AngularJS, your Javascript-based view layer also inherits features like Dependency-Injection, HTML-vocabulary extension (via the use of custom directives), unit-testing and functional testing integration as well as DOM-selectors ala JQuery (using jqlite as it provides only a subset of JQuery but you could also easily use JQuery if you prefer). AngularJS also introduces scopes to your Javascript code so that variables declared in your code are bound only to the scope that is required.This prevents variables pollution that inadvertently arises when the size of your Javascript grows.

When you are developing a Spring Web MVC application using JSP, you will likely use the Spring-provided form tags to bind your form inputs to a server side model. Similarly, AngularJS provides a way to bind form inputs to models on the client side. In fact, it provides instantaneous 2-way data-binding from the form input to your model on the Javascript application. That means that not only do you have the benefits of having your view updated with changes inside your Javascript model, any changes you make to your UI will also update the Javascript model (and consequently any other views that is bound to that model). It is almost magical to see all the views that are bound to the same JS model on the app update the model automatically.

Moreover, since your model can be set to a particular scope, only views that belong to the same scope will be affected, allowing you to sandbox code that should be local only to a particular portion of your view. (This is done via an AngularJS attribute calledng-controllerthat is set in your HTML templates). You can see the difference in a later section comparing JSP tags and AngularJS directives.

Two-way Data Binding

In a Spring-JSP web application, there is one way data binding from Spring model to jsp view. Any change to the model will be reflected to Jsp view but not the reverse. This is the nature of web applications. If we build a desktop application, it is possible to do reverse data binding with Swing UI.

However, for a web application exposing REST resources, there may be no direct data binding. Data is sent from the server to the browser as JSON objects. Without AngularJS and the like, developers need to write javascript code in order to bind javascript object to html controls.

Because manual data binding is a tedious task, some developers try to automate the task by creating a Javascript framework for data binding. It is worth remembering that this data binding happens on the client side and the model for data binding is a Javascript object rather than a server side model.

Angular pushes this idea further by creating a two-way binding. Changing values in an HTML control will be reflected in the object in real time.

Scope

Binding is a useful concept if you need to deal with complex UI components like AJAX tables.

For example: we need to render a list of users and roles in an AngularJs application, with the following html template:

<tr ng-repeat="user in users"> <td>{{user.username}}</td> <td>{{user.role}}</td> </tr> ... <a ng-click="addUser()">Add new user</a>

The code to add a user can be this simple:

$scope.addUser = function(){ newUser = {} $scope.users.push(newUser ); }

If the arrayusershas one more element, the table will automatically have one more row.

AngularJS Templates

Using AngularJS, it is possible to write relatively complex User Interfaces in an organized and elegant manner, always encapsulating the required logic within your components and never running the risk of errant global Javascript variables polluting your scope. It is also very testable, and there are built-in mechanisms to perform tests at the unit and functional level, ensuring that your User Interface codebase goes through the same rigorous testing that your Java/Spring code undergoes, ensuring quality even at the user interface level.

Another advantage of using AngularJS to write your html templates is that the templates are essentially similar to html even with the various front end logic baked into your view. It is possible to incorporate AngularJS logic into your template and still do a client-side validation control. In the JSP world, you can try viewing a JSP file from a browser with all the template logic in place and most likely your browser will give up rendering the page.
You can see how a typical AngularJS template looks like :

<div class="row thumbnail-wrapper"> <div data-ng-repeat="pet in currentOwner.pets" class="col-md-3"> <div class="thumbnail"> <img data-ng-src="images/pets/pet{{pet.id % 10 + 1}}.jpg" class="img-circle" alt="My Pet Image"> <div class="caption"> <h3 class="caption-heading" data-ng-bind="pet.name"></h3> <p class="caption-meta" data-ng-bind="pet.birthdate"></p> <p class="caption-meta"><span class="caption-label" data-ng-bind="pet.type.name"></span></p> </div> <div class="action-bar"> <a class="btn btn-default" data-toggle="modal" data-target="#petModal" data-ng-click="editPet(pet.id)"> <span class="glyphicon glyphicon-edit"></span> Edit Pet </a> <a class="btn btn-default"> <span></span> Add Visit </a> </div> </div> </div> </div>

You can probably spot some non-HTML additions to the template. It includes attributes likedata-ng-clickwhich maps a click on a button to a method name call. There’s alsodata-ng-repeatwhich loops through a JSON array and generates the necessary html code to render the same view for each item in the array. Yet with all the logic in place, we are still able to validate and view the html template from the browser.
AngularJS calls all the non-html tags and attributes “directives” and the purpose of these directives is to enhance the capabilities of HTML. AngularJS also supports both HTML 4 and 5 so if you have templates that are still relying on HTML 4 DOCTYPEs, it should still work fine (although the validators for HTML 4 will not recognize data-ng-x attributes).

One big difference between using AngularJS and JSP is the rendering time. If you use JSPs, the server renders html content. In contrast, if you use AngularJS, the rendering is happening in browser. Therefore, both the templates and JSON objects are to be sent to client side. It is worth to notice that AngularJS may briefly display the template before running DOM manipulation to generate content. For example, if AngularJS has not completed loaded, the date of birth in the page will be shown with an empty value before showing the real value.

Scopes in AngularJS

One important concept to grasp in AngularJS is that of scopes. In the past, whenever I had to write Javascript for my web application, I had to manage the variable names and construct special name-spaced objects in order to store my scoped properties. However, AngularJS does it for you automatically based on its MVx concept. Every directive will inherit a scope from its controller (or if you would like, an isolated scope that does not inherit other scope properties). The properties and variables created in this scope do not pollute the rest of the scopes or global context.

Scopes are used as the “glue” of an AngularJS application. Controllers in AngularJS use scopes to interact with the views. Scopes are also used to pass models and properties between directives and controllers. The advantage of this is that we are now forced to design our application in a way that components are self-contained and relationships between components have to be considered carefully through a use of a model that can be prototypically inherited from a parent scope.

A scope can be nested in another scope prototypically in the same way Javascript implements its inheritance model via prototyping. However, any property name that is declared in the child scope that is similar to the parent will hide the parent property from the child scope thereafter. An example of this can be described in the code below:

<!DOCTYPE html> <html> <head> <script data-require="angular.js@*" data-semver="1.4.0-rc.0" src="https://code.angularjs.org/1.4.0-rc.0/angular.js"></script> <link rel="stylesheet" href="style.css" /> <script src="script.js"></script> </head> <body data-ng-app="demo"> <h1>Scopes in AngularJS</h1> <div data-ng-controller="parentController"> <div data-ng-controller="childController"> <span>This is a demonstration of scopes</span> <div> Parent model: <span data-ng-bind="$parent.model.name"></span> </div> <div> Current model: <span data-ng-bind="model.name"></span> </div> <div> <button data-ng-click="updateModel()">Click me</button> </div> </div> </div> </body> </html>

At the very top in the hierarchy of scopes is the $rootScope, a scope that is accessible globally and can be used as the last resort to share properties and models across the whole application. The use of this should be minimized as it introduces a sort of “global” variable that can pose the same problems when it is overused.

More information about scopes can be gleaned from the AngularJS documentation found here.

Directives in AngularJS

Directives are one of the most important concepts in AngularJS. They bring all the additional customized markup in the HTML elements, attributes, classes or comments. They are the ones giving the markup new functionalities.

The following code snippet demonstrates a customized directive calledwdsCustomthat will replace the markup element<wds-custom company="wds">with markup that contains information about a model calledwds. That model element is declared in the controller scope that wraps the directive. You can have a look at the filesapp.js,index.htmland directive templatewds-custom-directive.htmlto see how this works in the plunkr snippet available here.

As this article does not attempt to teach you how to write a directive, you can refer to the official documentation here.

本文转载自:https://spring.io/blog/2015/08/19/migrating-a-spring-web-mvc-application-from-jsp-to-angularjs

共有 人打赏支持
m
粉丝 6
博文 86
码字总数 22525
作品 0
海淀
AngularJS入门(用ng-bind指令实现单向绑定)

不同于jquery只是一个加强和简化前端开发的类库,angularjs是一个完整web前端框架,所以学习曲线高了很多。 angularjs给我的感觉类似于Java的Spring框架,处于中心容器位置粘合其他组件,其内...

foodon
2014/10/29
0
0
AngularJS入门(用ng-repeat指令实现循环输出)

循环输出列表很多项目在web服务端做,前端做好模版后后端写jsp代码,双方需要紧密合作,分清责任。有些项目由后端提供restful方法,前端用ajax调用自己循环,这种一般是大把的jquery拼字符串...

foodon
2014/10/30
0
3
AngularJS跨域问题 ajax 跨域

先看代码: 一:案例实现 从网上下载了一个AngularJS项目,配置启动后发现数据发送不到自己的后台中去,总是提示跨域问题。 下面是AngularJS的部分代码: <!DOCTYPE html PUBLIC "-//W3C//D...

simpower
2017/12/13
0
0
通过共享文件夹来进行前后端独立开发

最近在快速开发一个后台系统,前端使用了bootstrap和angular搭建,后端使用Java,由于前端采用的是单页富应用的Web App的构建方式,所以不适合做JSP页面,最后采用了【前端+ajax+后台】的前后...

前端届的科比
2015/11/18
0
0
图表控件FusionWidgets​

FusionWidgets是一款功能强大的图表控件,同时支持Flash和JavaScript(HTML5),可用于PCs, Macs, iPads, iPhones 等,包含一系列实时仪表和自动更新图表以及金融图表,甘特图、funnel/pyramid图...

longboo
2014/06/19
0
0
Nginx+Tomcat部署Angular+javaweb项目的操作

导读 最近项目进入了即将验收阶段,项目部署是必不可少的。由于某些原因,我们把前端项目(Angular4)和后端项目(JavaWeb),由于要部署了四个项目:Angular4(微信端),微信端数据服务,JavaWeb...

问题终结者
04/14
0
0
OSChina 技术周刊第二十九期 —— HTTP 有时候比 HTTPS 好?

每周技术抢先看,总有你想要的! 移动开发 【软件】iOS 图表控件 ios-charts 【软件】跨平台应用开发框架 Bridge.NET 【博客】为什么不能往 Android 的 Application 对象里存储数据 【博客】...

OSC编辑部
2015/04/12
0
0
jFinal+AngularJs未来javaEE开发的趋势——程序员的福音

最近有意无意、机缘巧合之下认识了两个新的WEB框架,其中一个是后端框架叫JFinal,看名字就让人觉得为之一振,最后的、最终的,没错它的意思就是“我是JavaEE的终极框架”,没有比这更好的啦...

moz1q1
2014/08/05
0
2
Angular4 ngrxstore简介和安装

1 ngrx store 简介 对于ngrx store 在angualr2/4 的官方文档介绍不多。所以我在这里简单介绍一下ngrx store的概念。ngrx store 是借用redux 对于single page application的状态管理(state m...

vjjy001
06/26
0
0
OSChina 技术周刊第二十四期 —— C# 6.0 的新特性

每周技术抢先看,总有你想要的! 移动开发 【博客】Swift社交应用文本输入优化汇总 服务端开发/管理 【翻译】HTTP/1 的最佳实践并不适合 HTTP/2 【翻译】使用 AppDomain 存储实现大数据集合 ...

OSC编辑部
2015/03/09
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

about git flow

  昨天元芳做了git分支管理规范的分享,为了拓展大家关于git分支的认知,这里我特意再分享这两个关于git flow的链接,大家可以看一下。 Git 工作流程 Git分支管理策略   git flow本质上是...

qwfys
今天
2
0
Linux系统日志文件

/var/log/messages linux系统总日志 /etc/logrotate.conf 日志切割配置文件 参考https://my.oschina.net/u/2000675/blog/908189 dmesg命令 dmesg’命令显示linux内核的环形缓冲区信息,我们可...

chencheng-linux
今天
1
0
MacOS下给树莓派安装Raspbian系统

下载镜像 前往 树莓派官网 下载镜像。 点击 最新版Raspbian 下载最新版镜像。 下载后请,通过 访达 双击解压,或通过 unzip 命令解压。 检查下载的文件 ls -lh -rw-r--r-- 1 dingdayu s...

dingdayu
今天
1
0
spring boot使用通用mapper(tk.mapper) ,id自增和回显等问题

最近项目使用到tk.mapper设置id自增,数据库是mysql。在使用通用mapper主键生成过程中有一些问题,在总结一下。 1、UUID生成方式-字符串主键 在主键上增加注解 @Id @GeneratedValue...

北岩
今天
2
0
告警系统邮件引擎、运行告警系统

告警系统邮件引擎 cd mail vim mail.py #!/usr/bin/env python#-*- coding: UTF-8 -*-import os,sysreload(sys)sys.setdefaultencoding('utf8')import getoptimport smtplibfr......

Zhouliang6
今天
1
0
Java工具类—随机数

Java中常用的生成随机数有Math.random()方法及java.util.Random类.但他们生成的随机数都是伪随机的. Math.radom()方法 在jdk1.8的Math类中可以看到,Math.random()方法实际上就是调用Random类...

PrivateO2
今天
2
0
关于java内存模型、并发编程的好文

Java并发编程:volatile关键字解析    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在...

DannyCoder
昨天
1
0
dubbo @Reference retries 重试次数 一个坑

在代码一中设置 成retries=0,也就是调用超时不用重试,结果DEBUG的时候总是重试,不是0吗,0就不用重试啊。为什么还是调用了多次呢? 结果在网上看到 这篇文章才明白 https://www.cnblogs....

奋斗的小牛
昨天
2
0
数据结构与算法3

要抓紧喽~~~~~~~放羊的孩纸回来喽 LowArray类和LowArrayApp类 程序将一个普通的Java数组封装在LowArray类中。类中的数组隐藏了起来,它是私有的,所以只有类自己的方法才能访问他。 LowArray...

沉迷于编程的小菜菜
昨天
1
0
spring boot应用测试框架介绍

一、spring boot应用测试存在的问题 官方提供的测试框架spring-boot-test-starter,虽然提供了很多功能(junit、spring test、assertj、hamcrest、mockito、jsonassert、jsonpath),但是在数...

yangjianzhou
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部