Struts2学习

本节概览
1、搭建一个struts2项目
2、struts.xml
package -> action -> result
3、完成登录案例
1)重点使用实现Action接口和继承ActionSupport类这两种来创建Action类
2)Action中获取页面参数,使用属性驱动封装:只需要写属性,然后提供set方法

1、struts2是一个实现mvc模式的表现层框架
1)mvc=model+view+controller
2)struts2演变历史:在struts1框架基础上增加了webwork框架
是一个web层框架,跟struts1没有多大联系 struts2=webwork+struts1

2、struts2项目创建步骤
1)struts2搭建步骤
第一步:创建web工程,导入struts2的jar
(从struts-2.3.24-all/struts-2.3.24/apps/struts2-blank/中找)
第二步:在web.xml中配置核心过滤器StrutsPrepareAndExecuteFilter
第三步:在src目录下创建struts.xml核心配置文件
第四步:jsp页面准备,创建Action类,在struts2中作为controller层类,叫"Action类"
1)index.jsp
<a href="${pageContext.request.contextPath}/struts2">struts2入门</a>
2)Struts2Action
public class Struts2Action {
public void show() {
System.out.println("show hello struts2.....");
}
}
第五步:在struts.xml配置
<struts>
<package name="default" namespace="/" extends="struts-default">
<action name="struts2" class="cn.itheima.web.action.Struts2Action" method="show">
</action>

<action name="_" class="cn.itheima.web.action.{1}Action" method="{2}"></action>
</package>
<struts>

2)struts能够完成的功能
   (1)将页面的数据进行封装,在Action类中可以获取到
   (2)struts2能够完成页面的跳转
   (3)struts2能够完成将后台的数据封装到域中,然后可以在页面中通过表达式获取域中数据

3、配置文件加载顺序
(1)default.properties:在struts2-core/org.apache.struts2下 ->
(2)*struts-default.xml:在在struts2-core下
struts-plugin.xml:在struts2的插件包中,用来配置插件
struts.xml:我们自己工程的src下 ->
(3)struts.properties:我们自己工程的src下,用来定制常量
(4)自定义配置 -> (5)
web.xml中的关于struts2的配置 -> (6)bean相关配置

4、struts.xml配置语法学习(package-->action--->result)
1)package标签
<package name="名字随便取,但是在同一个struts.xml中不能重复"
namespace="这个名称和action中定义的name的值拼接成url"
extends="struts-default" >
</package>
2)action标签
<action name="在同一个package下名字不能重复"
class="你的Action类的完整类路径"
method="指明Action中要执行的方法">
</action>
当省略了class的时候,struts就会去找ActionSupport(在struts-default.xml中配置的);
当省略method,struts2默认去找Action类中的execute(),如果没有找到报错

3)result标签
  <result name="Action类中方法返回的字符串,俗称逻辑视图" 
      type="跳转的方式,默认的是转发,可以写dispatcher/redirect/redirectAction/chain">
    /succes.jsp
  </result>
    不配置type,的跳转方式是dispatcher
    (1)发一个action请求跳转到页面(转发dspatcher+重定向redirect)
    (2)发一个action请求跳转再次去请求action(转发chain+重定向redirectAction)
4)转发与重定向
    (1)转发发1次请求,重定向发2次请求;
    (2)转发浏览器地址url不变,重定向url地址发生变化;
    (3)转发可以带着值过去,重定向不能带着上一个请求的值过去。

5、struts2中Action类创建的三种方式---重点使用实现Action接口和继承ActionSupport类
1)直接写一个类
如:LoginAction
2)实现Action接口
LoginAction implements Action
3)继承ActionSupport类
LoginAction extends ActionSupport

6、struts2常量配置的几种方式(struts.xml,struts.properties,<init-param>)
<constant name="struts.action.extension" value="do"></constant>
<constant name="struts.devMode" value="true"></constant>
已经在default.properties文件已经定义了常量的值,为啥还需要在struts.xml中再次定义?
原因:修改默认的初始化常量值
小结---利用Struts2:
1、完成登录的功能
2、完成注册的功能
3、把登录、注册和jdbc、Oracle结合起来
===================================================================================================
本节概览:
1、页面参数,在Action中怎么封装?
2、ValueStack值栈
ValueStack放数据,Struts2标签+Ognl表达式取出值栈数据,并在页面显示

1、<result>标签重新认识
1)全局结果配置(struts.xml中配置)
<global-results>
<result name="success">/success.jsp</result>
<result name="error">/error.jsp</result>
</global-results>

2)全局结果配置和局部结果配置优先级
    当同时在<global-results>和<Action>中配置了相同name属性值的<result>,
    那么优先选择局部结果配置<result>

2、ServletActionContext和ActionContext2个类的使用(Ctrl+Shift+T查看代码)
1)ActionContext
ActionContext actionContext=ActionContext.getContext();
Map<String,Object> map=actionContext.getParameters(); //获取请求的所有参数
Application/Session/ValueStack...

2)ServletActionContext
    HttpServeltRequest request=ServletActionContext.getRequest();
    HttpServletResponse/PageContext....
    ServletActionContext extends ActionContext
上面的2个类可以从页面获取数据,也可以将数据封装到对象中    

A是一个类,B是一个接口,A实现了B,A instanceof B为true
A是一个类,B是一个类,A集成了B,A instanceof B为true

3、页面数据封装(重点掌握属性驱动和模型驱动)
1)属性驱动封装(Action类中成员变量+成员变量get/set方法+页面属性名)
(1)在Action类中写属性
比如Action类中private String username;然后提供set方法
在页面中name="username"
(2)在Action类中写private User user1;(包含username属性)对象,然后提供get/set方法
在页面中写法是:name="user1.username"

2)模型驱动封装
    (1)Action类实现ModelDriven接口
    (2)提供对象private User user = new User();
    (3)实现getModel方法
        @Override
        public User getModel() {
            return user;
        }
    (4)在页面中写法:name="username" 注意在页面中直接写模型驱动的关联对象的属性
        
 3)传统方式(ActionContext,ServletActionContext,了解)
    (1)先获取request对象,再从request中获取提交数据
        servletActionContext.getRequest();
        String username = request.getParameter("username");
    (2)直接获取提交的数据
        public class ServletActionContext extends ActionContext
        actionContext.getParameters(); 
        例如:
        ActionContext actionContext = ServletActionContext.getContext();
        Map<String, Object> parameters = actionContext.getParameters();
        for (String key : parameters.keySet()) {
            String[] v = (String[]) parameters.get(key);
            System.out.println("key:" + key + "-------value:" + v[0]);
        }

4、ValueStack----后台数据存放在某一个容器中,然后在页面中通过Struts2标签+Ognl表达式来获取值
1)ValueStack在什么时候被创建?
发action请求,创建一个Action对象,同时创建ActionContext及ValueStack值栈对象
总结:request、Action对象、ActionContext、ValueStack都在一次请求中

2)怎么获取ValueStack?
    可以通过request获取ValueStack
    也可以通过ActionContext获取ValueStack
    ActionContext ac=ActionContext.getContext();
    ValueStack vs=ac.getValueStack();

3)ValueStack内部结构(root+context)
    root底层结构是List,不要等同于OgnlContext中的root,因为Valuestack中的root对OgnlContext中的root进行了封装
    context底层结构是Map 
    通过context能够得到root

4)root和context中存放了哪些数据?
    root中:
        (1)发action请求的时候会将action对象放入到root
        (2)如果Action类实现了ModelDriven,会将模型驱动的model放入到root
        (3)其他自己手动放入root的数据
            vs.set(key,value);将key/value封装到了HashMap中,然后将该HashMap存放到ValueStack的root中
            vs.push(value);直接存到ValueStack的root中
    context中:web开发相关信息的引用
    
5)向ValueStack中root中存入数据,页面如何来通过Ognl表达式来获取
    <s:property value=""/> 这里的value怎么写?非常重要
    struts2的标签 + Ognl表达式来取ValueStack里的数据
    总结:
    (1)如果向ValueStack中的root存放了普通字符串 通过push()存入
        <s:property value="[0].top去取"/> 
    (2)如果向ValueStack中的root存放了bean     
        <s:property value="直接写属性名称"/> 
    (3)如果向ValueStack中的root存放了map集合 通过set(key,value) 
        <s:property value="直接写key"/>
    (4)如果向ValueStack中的root存放了list集合   
        <s:property value="[序号]去取"/>
    注意:当我们把数据存放在root中的时候,同时也向request中存放了一份,
         所以可以通过EL表达式获取数据
    
6)向ValueStack中的context存入数据,页面如何来通过Ognl表达式来获取数据
    把Valuestack中context理解成一个map,要想取value,需要通过"#key"来取
    <s:property value="#session.xxx"/>

1、值栈里有两个对象
1)这里的private User user = new User();对象会放入到Valuestack的Root中
2)当前Action类也会放入Valuestack的root中,而该Action类中有一个成员变量
名称为getModel的"model",值为getModel()返回的对象
2、问题:为什么页面传递的参数是username=tom&password=123,然后在execute方法中写如下代码:
user=new User();
user.setUsername("张三");
user.setPassword(456);
此时,root中user(username="tom",password=123),为什么没有被改成username=张三,password=456
3、原因:modeldrivenInterceptor拦截器中有一个参数refreshModelBeforeResult,默认是false,
也就是不刷新。如果设置成true,当Struts2检查放入栈顶的getModel返回对象不再是原来的对象
引用的时候,会重新刷新栈顶的元素,将老的对象删除,放入新的对象。
<interceptor-ref name="modelDriven">
<param name="refreshModelBeforeResult">true</param>
</interceptor-ref>

代码测试:
1、参数封装
1)模型驱动封装参数到实体类中,A对象里包含B对象,A对象叫做包装对象
2)属性封装如下参数:字符串,字符串数组,对象,集合(list,map )
2、描述Valustack是什么时候创建?
3、Valustack结构(root+context)
向root中存放了数据(字符串,对象,list集合,map集合),该怎么去取?
================================================================================================
本节概览:
1、属性驱动和模型驱动封装参数的补充说明
2、拦截器
3、文件上传
4、json


5、struts2的标签(重点掌握list集合和map集合)
1)展示错误提示信息
(1)让当前的Action类继承ActionSupprt
(2)Action类中添加信息
this.addActionError("用户名或密码错误");
this.addFieldError("username", "用户名错误");
this.addActionMessage("LoginAction信息");
(3)页面标签获取信息
<s:actionerror/>
<s:fielderror fieldName="username"/>
<s:actionmessage/>

2)开发调试
    <s:debug/>
    
3)Ognl表达式取值
    <s:property value=""/>
    
4)循环遍历取值展示
    第1种写法:
    <s:iterator value="ps">
        <tr>
            <td><s:property value="name" /></td>
            <td><s:property value="price" /></td>
            <td><s:property value="count" /></td>
        </tr>
    </s:iterator>
    注意:如果没有加"var",每次取的是每一份product,只需要写product的属性就可以取到
    
    第2种写法:
    <s:iterator value="ps" var="p">
        <tr>
            <td><s:property value="#p.name" /></td>
            <td><s:property value="#p.price" /></td>
            <td><s:property value="#p.count" /></td>
        </tr>
    </s:iterator>
    注意:如果加了"var",每次遍历的时候,会将var代表的每一份product对象放入Valuestack的context中。
    这里product放入到context中,key=p,value=product
    
    遍历map集合第1种方式
    <s:iterator value="map1.keySet" var="k">
        <s:property value="#k" />
        <s:property value="map1.get(#k).name" />
        <s:property value="map1.get(#k).price" />
        <s:property value="map1.get(#k).count" />
        <br />
    </s:iterator>
    
    遍历map集合第2种方式
    <s:iterator value="map1.entrySet()" var="entry">
        <s:property value="#entry.getKey()" />
        <s:property value="#entry.getValue().name" />
        <s:property value="#entry.getValue().price" />
        <s:property value="#entry.getValue().count" />
        <br />
    </s:iterator>

6、struts2中拦截器
1)struts2多个拦截器执行理解
当多个拦截器或者多个过滤器一起作用的时候,其实运用的是java设计模式中的"责任链模式"
struts2底层就是有很多拦截器帮助action类来实现多个不同的功能,如modeldriven拦截器,如validate校验拦截器,如exception处理拦截器等
这一个个拦截器完成不同的功能

2)实现自定义拦截器的两种方式
    第一种方式:
        第一步:创建一个拦截器类 public class MyInterceptor implements Interceptor
            或者也可以MyInterceptor extends AbstractInterceptor
        第二步:在struts.xml中配置拦截器
            <interceptors>
                <interceptor name="myInterceptor" class="cn.itheima.utils.MyInterceptor">
            </interceptors>
        第三步:哪个Action类需要拦截器,就在对应的struts.xml中引用
            <action name="product_*" class="cn.itheima.action.ProductAction" method="{1}">
                <result name="success">/product.jsp</result>
                <result name="login">/login.jsp</result>
                <interceptor-ref name="myInterceptor"/> 
                <!-- 注意:当我们自定义了interceptor-ref,struts2框架默认的拦截器栈会失效,
                    需要我们再配置一下
                -->
                <interceptor-ref name="defaultStack"></interceptor-ref>             
            </action>
        
    第二种方式:
        MyInterceptor extends MethodFilterInterceptor
        (配置参数:excludeMethods,includeMethods)
        struts.xml如下:
        <package>
           <interceptors>
                <interceptor name="myInterceptor" class="cn.itheima.utils.MyInterceptor">
                    <param name="includeMethods">showProduct</param> <!-- 拦截该方法-->
                    <param name="excludeMethods">addProduct</param>  <!-- 不拦截该方法-->
                </interceptor>
                <!--创建拦截器栈-->
                <interceptor-stack name="myStack">
                    <interceptor-ref name="myInterceptor"/>
                    <interceptor-ref name="defaultStack"></interceptor-ref> 
                </interceptor-stack>
            </interceptors> 
            <action name="" class="" method="">
               <interceptor-ref name="">//只有先定义,才能被引入
            </action>
        </package>
    
3)拦截器拦截的时间
    在请求action之前,拦截器去拦截
    注意:
    (1)invocation.invoke();这里放行,然后再去执行action中的方法showProduct()
    (2)当权限不足的时候,拦截器可以直接返回一个字符串,如"login",代表是<result name="login">
      这里不会执行到showProduct(),但是最终还是需要返回一个逻辑视图,就是result的name

Struts2三个核心知识点:
1、Action封装请求参数
1)属性驱动
(1)基本属性
private String username;
set方法
<form>
<input type="text" name="username">
</form>
(2)对象
private User user;
get/set方法
<form>
<input type="text" name="user.username">
</form>
(3)字符串数组
private String[] strs;
get/set方法
<form>
<select name="strs">
<option value="111"></option>
<option value="222"></option>
</select>
</form>
(4)list集合
private List<User> userList;
get/set方法
<form>
<input type="text" name="userList[0].username">
</form>
(5)map集合
private Map<String,User> userMap;
get/set方法
<input type="text" name="userMap['one'].username">

    2)模型驱动
        public class TestParameterAction4 implements Action,ModelDriven<UserVo> 
        包装对象是A,A中有B,B的对象名.B的属性名
        <form action="${pageContext.request.contextPath}/test4" method="post">
            username:<input type="text" name="user.username"><br>
            password:<input type="text" name="user.password"><br>
            <input type="submit" value="LOGIN">
        </form>
        
2、数据存储以及数据展示
    ValueStack
    存储及展示
        set(key,value); <s:property value="key的名字"/>
        push(Object);   <s:property value="[0].top"/> 栈顶
        Action类中的某个对象User    <s:property value="username"/>
        Action类中的list集合对象  <s:property  value="ps[0]" />
                              <s:iterator value="ps">
        Action类中的map集合对象 <s:iterator value="map1.keySet" var="k">
3、拦截器
    与Action类相独立
    扩展程序功能

1、struts2文件上传步骤(需要做实验,来体会,基于拦截器,我们需要学习拦截)
准备工作:导入上传文件相关的jar包
1)post提交,enctype=multipart/form-data,input type="file" name="upload"(必满足以上的规则)

2)在action类中声明如下成员变量
    private File upload; // 上传的文件
    private String uploadContentType; // 上传文件的mimeType类型
    private String uploadFileName; // 上传文件的名称
    
3)使用FileUtils类完成copy动作,其实就是读传过来的文件,写到指定磁盘路径

4)解决上传文件过大(>2m 默认大小)
    上传的文件首先读struts.multipart.maxSize常量设置大小
    再读上传文件拦截器<param name="maximumSize"></param>设置大小
  
5)如果想要把异常错误信息在标签中展示,那么需要对当前action类中的result配置input视图
    <action name="upMany" class="cn.itheima.action.UploadManyAction"
        method="uploadFile">
        <result name="input">/error.jsp</result>
    </action>
    
6)给拦截器设置参数语法
    <interceptor-ref name="validation">
        <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
注意:上传多个文件需要使用List<File>或者File[]来接收

2、掌握常用的2个json转换工具(需要熟悉2个工具类)
主要作用:把java对象转成json字符串,把json字符串转成java对象
1)fastjson
(掌握)核心类:JSONArray+JSONObject+JSON
注解:@JSONField 作用于bean的属性
过滤器:属性过滤器PropertyFilter(该过滤器是一个接口)

2)jackjson
    (掌握)核心类:ObjectMapper 通过write方法来完成java对象转换成json string
    注解:@JSON @JsonIgnore  @JsonFilter
    过滤器:FilterProvider 一个Filter的容器  

3、通过ServeltActionContext类获的Request,Response
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
获取request可以将页面提交的数据封装
ServletActionContext.getRequest().getParameter("username");
获取response可以将数据返回给页面
ServletActionContext.getResponse().getWriter().write(返回页面的数据);
注意:模型驱动封装数据和成员变量提供get方法,这两种方式其底层也是通过
ServeltActionContext获取request来获取参数数据,并封装成bean

代码测试::
1、封装参数的方式(熟悉)
2、ValueStack存数据及页面显示数据
3、显示商品需要先登录的案例
4、文件上传
5、写2个xx.xml,然后在struts.xml中通过include引入
是否要求这2个xx.xml中的package的名字要唯一
通过实验证实:
如果在同一个struts.xml中可以写同一个name的package,struts2 优先选择上面一个package,
下面一个package不起作用
如果在同一个struts.xml中通过include标签引入其他相同name的package,
那么优先选择include相同名称的package
==============================================================================================
1、struts2的json的插件(掌握,struts2是如何使用一个插件)
1)实现步骤
导入struts2-json-plugin.jar
在<package>中extends="json-default"
在<result>中type=json
将需要转换的Object放入到ValueStack中的root中(俗称入栈)
2)result返回json过滤配置
通过查看struts-json-plugin.jar中JSONInteceptor源码,可以知道
通过root/excludeProperties等参数可以实现对转换json进一步过滤
<param name="root">ps</param> 表示取出ps对应的json数据
<param name="excludeProperties">[\d+].releaseDate</param>

    {"ps":[{"id":1,"name":"电视机","price":2000.0,"releaseDate":"2018-03-12T11:01:06"},
            {"id":2,"name":"电冰箱","price":3000.0,"releaseDate":"2018-03-12T11:01:06"}],
        "com.opensymphony.xwork2.util.OgnlValueStack.MAP_IDENTIFIER_KEY":""}
    
    [{"id":1,"name":"电视机","price":2000.0,"releaseDate":"2018-03-12T10:59:18"},
    {"id":2,"name":"电冰箱","price":3000.0,"releaseDate":"2018-03-12T10:59:18"}]
    
    ps[0].releaseDate  ps[1].releaseDate
    [0].releaseDate  [1].releaseDate
    
    注意:关于excludeProperties配置的正则表达式,要求看得懂即可。

2、struts2实现注解转换插件(掌握)
1)关于注解,就是一些约定、规范
(1)注解已经成为了一个种开发趋势,替代了传统的xml配置。
ssh+ssm,都有注解提供的,比较流行的框架spring boot也有很多注解
注意:任何一个注解的定义及其使用,必然底层有对该注解的解释,
通过解析可以获取到注解上面配置的参数值。
(2)关于注解正确学习方法:
点击进去了解该注解哪个地方使用
有哪些属性,而且该属性的类型你必须对应,是什么类型就加什么类型的值

2)实现步骤
    导入struts2-convention-plugin.jar
    在Action类上加上@ParentPackage @Namespace
    在Action类中方法上加@Action
    通过@Action中的属性results和interceptorRefs来声明
    @Result和@interceptorRef
    
3)注解扫描规则
    为了让插件知道哪些类或者方法上加了它的注解进而实现注解的功能
    所以,需要同时满足以下规则即可被扫描到:
    在action类所在包(父包,子包)中包含action/actions/struts/struts2之一
    action的类命名上必须以Action后缀或者该Action类上实现 com.opensymphony.xwork2.Action接口
    在struts2-convention-plugin-xxx.jar可以找到
    <constant name="struts.convention.package.locators" value="action,actions,struts,struts2"/>
    <constant name="struts.convention.action.suffix" value="Action"/>
    注意注解扫描规则,因为后面在学习spring的也有注解扫描规则

3、代码练习
@Action(value = "showProduct", interceptorRefs = { @InterceptorRef("myStack") }
,results = {@Result(name="success",type="json")})
public String showProduct() {
...
ActionContext.getContext().getValueStack().push(result);
return "success";
}
product.jsp
var obj = eval(data);
var obj = data; 可以不需要写eval函数,直接得到的就是一个js对象

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345