人人快速开发平台 renren-fast 源码分析(二)异常处理和校验机制

异常处理

在上一篇文章中,我们找到了RRExceptionHandler

/**
 * 异常处理器
 * 
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2016年10月27日 下午10:16:19
 */
@RestControllerAdvice
public class RRExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 处理自定义异常
     */
    @ExceptionHandler(RRException.class)
    public R handleRRException(RRException e){
        R r = new R();
        r.put("code", e.getCode());
        r.put("msg", e.getMessage());

        return r;
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    public R handlerNoFoundException(Exception e) {
        logger.error(e.getMessage(), e);
        return R.error(404, "路径不存在,请检查路径是否正确");
    }

    @ExceptionHandler(DuplicateKeyException.class)
    public R handleDuplicateKeyException(DuplicateKeyException e){
        logger.error(e.getMessage(), e);
        return R.error("数据库中已存在该记录");
    }

    @ExceptionHandler(AuthorizationException.class)
    public R handleAuthorizationException(AuthorizationException e){
        logger.error(e.getMessage(), e);
        return R.error("没有权限,请联系管理员授权");
    }

    @ExceptionHandler(Exception.class)
    public R handleException(Exception e){
        logger.error(e.getMessage(), e);
        return R.error();
    }
}

该类用@RestControllerAdvice修饰,这个注解修饰的类,里面的方法作用于@ResponseBody修饰的方法返回结果。从handleRRException(RRException e)方法看得出,系统中的业务逻辑异常都是直接往上抛,最后由RRExceptionHandler处理并且返回对应的提示信息。
查看R类可以发现,该项目 RESTful 接口返回结果都是一个统一格式的 map 转成的 json

/**
 * 返回数据
 * 
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2016年10月27日 下午9:59:27
 */
public class R extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;
    
    public R() {
        put("code", 0);
        put("msg", "success");
    }
    
    public static R error() {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
    }
    
    public static R error(String msg) {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }
    
    public static R error(int code, String msg) {
        R r = new R();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public static R ok(String msg) {
        R r = new R();
        r.put("msg", msg);
        return r;
    }
    
    public static R ok(Map<String, Object> map) {
        R r = new R();
        r.putAll(map);
        return r;
    }
    
    public static R ok() {
        return new R();
    }

    public R put(String key, Object value) {
        super.put(key, value);
        return this;
    }
}

校验

项目中使用的是 Hibernate-validator 做输入的校验
以登录表单为例
LoginForm.java

/**
 * 登录表单
 *
 * @author Mark sunlightcs@gmail.com
 * @since 3.1.0 2018-01-25
 */
@ApiModel(value = "登录表单")
public class LoginForm {
    @ApiModelProperty(value = "手机号")
    @NotBlank(message="手机号不能为空")
    private String mobile;

    @ApiModelProperty(value = "密码")
    @NotBlank(message="密码不能为空")
    private String password;

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

AppLoginController.java

/**
 * APP登录授权
 *
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-03-23 15:31
 */
@RestController
@RequestMapping("/app")
@Api("APP登录接口")
public class AppLoginController {
    @Autowired
    private UserService userService;
    @Autowired
    private JwtUtils jwtUtils;

    /**
     * 登录
     */
    @PostMapping("login")
    @ApiOperation("登录")
    public R login(@RequestBody LoginForm form){
        //表单校验
        ValidatorUtils.validateEntity(form);

        //用户登录
        long userId = userService.login(form);

        //生成token
        String token = jwtUtils.generateToken(userId);

        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        map.put("expire", jwtUtils.getExpire());

        return R.ok(map);
    }

}

ValidatorUtils.java

/**
 * hibernate-validator校验工具类
 *
 * 参考文档:http://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/
 *
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-03-15 10:50
 */
public class ValidatorUtils {
    private static Validator validator;

    static {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    /**
     * 校验对象
     * @param object        待校验对象
     * @param groups        待校验的组
     * @throws RRException  校验不通过,则报RRException异常
     */
    public static void validateEntity(Object object, Class<?>... groups)
            throws RRException {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        if (!constraintViolations.isEmpty()) {
            StringBuilder msg = new StringBuilder();
            for(ConstraintViolation<Object> constraint:  constraintViolations){
                msg.append(constraint.getMessage()).append("<br>");
            }
            throw new RRException(msg.toString());
        }
    }
}

参照 hibernate-validator文档 ,得知,在AppLoginControllerlogin 方法中,调用ValidatorUtils.validateEntity(form);通过 LoginForm声明的属性对应的@NotBlank注解,来校验输入是否符合约束。若不符合,根据 validator 返回的message拼接成异常信息,然后抛出 RRException,提示对应信息,这就连接上异常处理功能了。

异常处理和校验,如果只是看怎么使用,为什么要这么用的话是挺简单的。项目中使用@RestControllerAdvice作为异常处理的方式,并且用 hibernate-validator 作为校验工具,可以减少大量的输入校验和异常捕获的代码(当然有些时候还是需要自己在代码中捕获不希望抛出的异常的)。

但是只看怎么用实在是太没有意思了。因此我们来试着读一下 validator 的源码~

首先我们找到 Validator 的实现类 ValidatorImpl,找到validate(T object, Class<?>... groups)方法

    @Override
    public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
        Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
        sanityCheckGroups( groups );

        ValidationContext<T> validationContext = getValidationContextBuilder().forValidate( object );

        if ( !validationContext.getRootBeanMetaData().hasConstraints() ) {
            return Collections.emptySet();
        }

        ValidationOrder validationOrder = determineGroupValidationOrder( groups );
        ValueContext<?, Object> valueContext = ValueContext.getLocalExecutionContext(
                validatorScopedContext.getParameterNameProvider(),
                object,
                validationContext.getRootBeanMetaData(),
                PathImpl.createRootPath()
        );

        return validateInContext( validationContext, valueContext, validationOrder );
    }

根据官网文档指出,groups 不传的话应该是对所有属性进行校验,否则只对对应 groups 的属性进行校验。那我们就来看看它是怎么做到的.
在这个方法中主要做了四件事:

  1. 根据 object 参数获得 ValidationContext<T>
  2. 根据 groups 参数获得ValidationOrder
  3. 根据 object, validationContext 等获得ValueContext<?, Object>
  4. 调用validateInContext( validationContext, valueContext, validationOrder );

这个方法的主干就完成了,接下来一个个方法看看里面都做了什么

根据 object 参数获得 ValidationContext<T>

查看ValidationContext<T>源码,类的注释上有这么一段话

/**
 * Context object keeping track of all required data for a validation call.
 *
 * We use this object to collect all failing constraints, but also to have access to resources like
 * constraint validator factory, message interpolator, traversable resolver, etc.
 */

也就说,这个类是用来做 validation call 的时候传入的,并且会把调用时产生的所有需要的数据都追踪。这个类是通过ValidationContextBuilder.forValidate(T rootBean)创建的。看看都是怎么创建的吧。

public <T> ValidationContext<T> forValidate(T rootBean) {
            @SuppressWarnings("unchecked")
            Class<T> rootBeanClass = (Class<T>) rootBean.getClass();
            return new ValidationContext<>(
                    ValidationOperation.BEAN_VALIDATION,
                    constraintValidatorManager,
                    constraintValidatorFactory,
                    validatorScopedContext,
                    traversableResolver,
                    constraintValidatorInitializationContext,
                    rootBean,
                    rootBeanClass,
                    beanMetaDataManager.getBeanMetaData( rootBeanClass ),
                    null, //executable
                    null, //executable parameters
                    null, //executable return value
                    null //executable metadata
            );
        }

已知传入的参数有待校验的 bean,这个 bean 所属的 class,还有一堆杂七杂八的参数,先放着。目前已知这个类是用来存放所有校验时的数据的就行了。

根据 groups 参数获得 ValidationOrder

跟踪 determineGroupValidationOrder( groups ) 会找到getValidationOrder(Collection<Class<?>> groups)

/**
     * Generates a order of groups and sequences for the specified validation groups.
     *
     * @param groups the groups specified at the validation call
     *
     * @return an instance of {@code ValidationOrder} defining the order in which validation has to occur
     */
    public ValidationOrder getValidationOrder(Collection<Class<?>> groups) {
        if ( groups == null || groups.size() == 0 ) {
            throw LOG.getAtLeastOneGroupHasToBeSpecifiedException();
        }

        // HV-621 - if we deal with the Default group we return the default ValidationOrder. No need to
        // process Default as other groups which saves several reflection calls (HF)
        if ( groups.size() == 1 && groups.contains( Default.class ) ) {
            return ValidationOrder.DEFAULT_GROUP;
        }

        for ( Class<?> clazz : groups ) {
            if ( !clazz.isInterface() ) {
                throw LOG.getGroupHasToBeAnInterfaceException( clazz );
            }
        }

        DefaultValidationOrder validationOrder = new DefaultValidationOrder();
        for ( Class<?> clazz : groups ) {
            if ( Default.class.equals( clazz ) ) { // HV-621
                validationOrder.insertGroup( Group.DEFAULT_GROUP );
            }
            else if ( isGroupSequence( clazz ) ) {
                insertSequence( clazz, clazz.getAnnotation( GroupSequence.class ).value(), true, validationOrder );
            }
            else {
                Group group = new Group( clazz );
                validationOrder.insertGroup( group );
                insertInheritedGroups( clazz, validationOrder );
            }
        }

        return validationOrder;
    }

然后我们看看默认的 ValidationOrder

class DefaultGroupValidationOrder implements ValidationOrder {

        private final List<Group> defaultGroups;

        private DefaultGroupValidationOrder() {
            defaultGroups = Collections.singletonList( Group.DEFAULT_GROUP );
        }

        @Override
        public Iterator<Group> getGroupIterator() {
            return defaultGroups.iterator();
        }

        @Override
        public Iterator<Sequence> getSequenceIterator() {
            return Collections.<Sequence>emptyIterator();
        }

        @Override
        public void assertDefaultGroupSequenceIsExpandable(List<Class<?>> defaultGroupSequence) throws GroupDefinitionException {
        }
    }

可以看出ValidationOrder是用来存放校验的顺序的。里面可以取得一个校验迭代器,估计后面校验的时候是根据这个迭代器来比较属性的 group 的,这其实看类名就可以猜到了。

根据 object, validationContext 等获得 ValueContext<?, Object>

我们看看ValueContext<?, Object>是个什么东西

/**
 * An instance of this class is used to collect all the relevant information for validating a single class, property or
 * method invocation.
 */

根据类注释可知这个类是用来存放待校验的 property 和 method invocation 的。也就是说这个类是校验时用来遍历的。

那么这三个类是做什么的都知道了,我们看看校验逻辑

调用validateInContext( validationContext, valueContext, validationOrder );

/**
     * Validates the given object using the available context information.
     * @param validationContext the global validation context
     * @param valueContext the current validation context
     * @param validationOrder Contains the information which and in which order groups have to be executed
     *
     * @param <T> The root bean type
     *
     * @return Set of constraint violations or the empty set if there were no violations.
     */
    private <T, U> Set<ConstraintViolation<T>> validateInContext(ValidationContext<T> validationContext, ValueContext<U, Object> valueContext,
            ValidationOrder validationOrder) {
        if ( valueContext.getCurrentBean() == null ) {
            return Collections.emptySet();
        }

        BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
        if ( beanMetaData.defaultGroupSequenceIsRedefined() ) {
            validationOrder.assertDefaultGroupSequenceIsExpandable( beanMetaData.getDefaultGroupSequence( valueContext.getCurrentBean() ) );
        }

        // process first single groups. For these we can optimise object traversal by first running all validations on the current bean
        // before traversing the object.
        Iterator<Group> groupIterator = validationOrder.getGroupIterator();
        while ( groupIterator.hasNext() ) {
            Group group = groupIterator.next();
            valueContext.setCurrentGroup( group.getDefiningClass() );
            validateConstraintsForCurrentGroup( validationContext, valueContext );
            if ( shouldFailFast( validationContext ) ) {
                return validationContext.getFailingConstraints();
            }
        }
        groupIterator = validationOrder.getGroupIterator();
        while ( groupIterator.hasNext() ) {
            Group group = groupIterator.next();
            valueContext.setCurrentGroup( group.getDefiningClass() );
            validateCascadedConstraints( validationContext, valueContext );
            if ( shouldFailFast( validationContext ) ) {
                return validationContext.getFailingConstraints();
            }
        }

        // now we process sequences. For sequences I have to traverse the object graph since I have to stop processing when an error occurs.
        Iterator<Sequence> sequenceIterator = validationOrder.getSequenceIterator();
        while ( sequenceIterator.hasNext() ) {
            Sequence sequence = sequenceIterator.next();
            for ( GroupWithInheritance groupOfGroups : sequence ) {
                int numberOfViolations = validationContext.getFailingConstraints().size();

                for ( Group group : groupOfGroups ) {
                    valueContext.setCurrentGroup( group.getDefiningClass() );

                    validateConstraintsForCurrentGroup( validationContext, valueContext );
                    if ( shouldFailFast( validationContext ) ) {
                        return validationContext.getFailingConstraints();
                    }

                    validateCascadedConstraints( validationContext, valueContext );
                    if ( shouldFailFast( validationContext ) ) {
                        return validationContext.getFailingConstraints();
                    }
                }
                if ( validationContext.getFailingConstraints().size() > numberOfViolations ) {
                    break;
                }
            }
        }
        return validationContext.getFailingConstraints();
    }

这个方法主要调用了validateConstraintsForCurrentGroup( validationContext, valueContext );,那么继续看源码

private void validateConstraintsForCurrentGroup(ValidationContext<?> validationContext, ValueContext<?, Object> valueContext) {
        // we are not validating the default group there is nothing special to consider. If we are validating the default
        // group sequence we have to consider that a class in the hierarchy could redefine the default group sequence.
        if ( !valueContext.validatingDefault() ) {
            validateConstraintsForNonDefaultGroup( validationContext, valueContext );
        }
        else {
            validateConstraintsForDefaultGroup( validationContext, valueContext );
        }
    }

看看默认 group 的校验

private <U> void validateConstraintsForDefaultGroup(ValidationContext<?> validationContext, ValueContext<U, Object> valueContext) {
        final BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
        final Map<Class<?>, Class<?>> validatedInterfaces = new HashMap<>();

        // evaluating the constraints of a bean per class in hierarchy, this is necessary to detect potential default group re-definitions
        for ( Class<? super U> clazz : beanMetaData.getClassHierarchy() ) {
            BeanMetaData<? super U> hostingBeanMetaData = beanMetaDataManager.getBeanMetaData( clazz );
            boolean defaultGroupSequenceIsRedefined = hostingBeanMetaData.defaultGroupSequenceIsRedefined();

            // if the current class redefined the default group sequence, this sequence has to be applied to all the class hierarchy.
            if ( defaultGroupSequenceIsRedefined ) {
                Iterator<Sequence> defaultGroupSequence = hostingBeanMetaData.getDefaultValidationSequence( valueContext.getCurrentBean() );
                Set<MetaConstraint<?>> metaConstraints = hostingBeanMetaData.getMetaConstraints();

                while ( defaultGroupSequence.hasNext() ) {
                    for ( GroupWithInheritance groupOfGroups : defaultGroupSequence.next() ) {
                        boolean validationSuccessful = true;

                        for ( Group defaultSequenceMember : groupOfGroups ) {
                            validationSuccessful = validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz,
                                    metaConstraints, defaultSequenceMember );
                        }
                        if ( !validationSuccessful ) {
                            break;
                        }
                    }
                }
            }
            // fast path in case the default group sequence hasn't been redefined
            else {
                Set<MetaConstraint<?>> metaConstraints = hostingBeanMetaData.getDirectMetaConstraints();
                validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz, metaConstraints,
                        Group.DEFAULT_GROUP );
            }

            validationContext.markCurrentBeanAsProcessed( valueContext );

            // all constraints in the hierarchy has been validated, stop validation.
            if ( defaultGroupSequenceIsRedefined ) {
                break;
            }
        }
    }

这下应该就一目了然了,如果这个 class 被重新定义成 default group 那么遍历所有的这个类的层次结构。然后获取到这个类相关的所有MetaConstraint,传给下一个方法validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz, metaConstraints, defaultSequenceMember );。看MetaConstraint这个类可知,它是对应的一种约束,比如 Email, String等等。

private <U> boolean validateConstraintsForSingleDefaultGroupElement(ValidationContext<?> validationContext, ValueContext<U, Object> valueContext, final Map<Class<?>, Class<?>> validatedInterfaces,
            Class<? super U> clazz, Set<MetaConstraint<?>> metaConstraints, Group defaultSequenceMember) {
        boolean validationSuccessful = true;

        valueContext.setCurrentGroup( defaultSequenceMember.getDefiningClass() );

        for ( MetaConstraint<?> metaConstraint : metaConstraints ) {
            // HV-466, an interface implemented more than one time in the hierarchy has to be validated only one
            // time. An interface can define more than one constraint, we have to check the class we are validating.
            final Class<?> declaringClass = metaConstraint.getLocation().getDeclaringClass();
            if ( declaringClass.isInterface() ) {
                Class<?> validatedForClass = validatedInterfaces.get( declaringClass );
                if ( validatedForClass != null && !validatedForClass.equals( clazz ) ) {
                    continue;
                }
                validatedInterfaces.put( declaringClass, clazz );
            }

            boolean tmp = validateMetaConstraint( validationContext, valueContext, valueContext.getCurrentBean(), metaConstraint );
            if ( shouldFailFast( validationContext ) ) {
                return false;
            }

            validationSuccessful = validationSuccessful && tmp;
        }
        return validationSuccessful;
    }

validateConstraintsForSingleDefaultGroupElement()方法,了解方法主要是遍历metaConstraints,每个metaConstraint都对这个 group 进行一次校验。

往下读得知,它先是逐步创建约束对应的 constraintTree ,每个 constraintTree 用一个工厂类创建了 ConstraintValidator<A, ?> ,然后调用 isValid()方法,完成了校验。最后如果校验失败了,将不符合约束的地方写到了ValidationContext<T>中。每个constraintTree会校验目标对象的所有指定约束的属性。比如@Email

protected final <T, V> Set<ConstraintViolation<T>> validateSingleConstraint(ValidationContext<T> executionContext,
            ValueContext<?, ?> valueContext,
            ConstraintValidatorContextImpl constraintValidatorContext,
            ConstraintValidator<A, V> validator) {
        boolean isValid;
        try {
            @SuppressWarnings("unchecked")
            V validatedValue = (V) valueContext.getCurrentValidatedValue();
            isValid = validator.isValid( validatedValue, constraintValidatorContext );
        }
        catch (RuntimeException e) {
            if ( e instanceof ConstraintDeclarationException ) {
                throw e;
            }
            throw LOG.getExceptionDuringIsValidCallException( e );
        }
        if ( !isValid ) {
            //We do not add these violations yet, since we don't know how they are
            //going to influence the final boolean evaluation
            return executionContext.createConstraintViolations(
                    valueContext, constraintValidatorContext
            );
        }
        return Collections.emptySet();
    }

总结一下校验方法做了这些事:

  1. 如果传 groups ,就根据 groups 获取 group sequence,否则就是获取 default group sequence,也就是所有属性都要遍历都要校验。
  2. 每个 group 都进行一次 metaConstraints 的遍历,metaConstraint 对应一种约束,比如 Email 类型,或者 String 类型等等
  3. metaConstraint 拿到 constraintTree,然后创建 Validator,进行校验,如果校验不通过写到 ValidationContext 中
  4. 从 ValidationContext 中可以获取到校验不通过的所有信息

大概的校验机制就是这些了。

总结

读源码经常会被一层又一层的调用难倒,以我浅薄的经验,读源码首先要明确要读的是什么模块什么功能,带着目的性读源码,然后多从方法名和变量名去理解含义,多看类和接口的注释。逐步看懂细节再看整体,这样比较能对整个功能模块有全面的了解。

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,549评论 18 399
  • 村官骂领导“奴才”能以侮辱罪网上追逃? 作者 唐孝忠 据人民网消息,安徽灵璧县娄庄镇娄庄居委会主任、支书,县人大代...
    坚守良知阅读 512评论 0 1
  • 下午:团队聚会,讨论产品设计。 与通用航空杨总会面。杨总在与传媒大学筹备影视纪录片资源对接平台方面的项目。拟合作摄...
    hmisty阅读 123评论 0 0
  • 素描课从昨天下午两点至七点半,没有过程图,没想起来拍。中间有老师示范,还有自己画的时间,五个半小时,虽然最后注意力...
    雪时阅读 152评论 0 0