jackson注解-JPA-事务处理

Jackson注解

如果是POJO对象,有Getter和Setter方法,才能序列化和反序列化;

  • @JsonInclude

@JsonInclude(JsonInclude.Include.NON_EMPTY),如果字段是空,就忽略显示。如果是数组为空,显示[]。对象为空,显示{}。这是最佳实践

  • @JsonIgnore

忽略该字段,序列化和反序列化都生效

  • 类注解 @JsonIgnoreProperties

@JsonIgnoreProperties(value = {“refreshTime”}, allowGetters = true)。属性refreshTime, 序列化生效,反序列化不生效。可以对多个属性统一处理。

  • @JsonFormat

指定时间的格式,@JsonFormat(timezone = “GMT+8″, pattern = “yyyy/MM/dd HH:mm:ss”)可以使用在LocalDateTime Instant Date对象上。

注意:序列化和反序列化都是这个格式。如果没有指定格式,则为格林尼治格式。最佳实践,为时间类型对属性指定格式。

  • @JsonValue

使用此注解时,序列化当前属性的值,对象(toString),忽略其他属性。

  • @JsonAlias(“myPage”)

反序列化的时候,将JSON属性myPage映射POJO上,序列化的不受影响。

  • @JsonProperty

@JsonProperty(value = “myRow”, access = JsonProperty.Access.READ_WRITE)
反序列的时候,将JSON属性myRow映射POJO上,序列的属性值是myRow。 这个地方是跟@JsonAlias("myRow")的区别

access控制属性的读写,功能类似@JsonIgnorePropertiesallowGetters = true和allowSetters = true方法

  • @JsonView

在controller中使用注解@JsonView(Refresh.addressDetail.class),表示「只」序列化POJO中有@JsonView(Refresh.addressDetail.class)注解的属性。对选择性序列化有用

  • @JsonSerialize(using = AddressJsonSerializer.class)

指定序列化:address对象序列化成字符串

public class AddressJsonSerializer extends JsonSerializer<Address> {

    @Override
    public void serialize(Address address, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(address.toString());
    }
}
  • @JsonDeserialize(using = PhoneJsonDeserializer.class)

指定反序列化:字符串0523-09024434反序列化成phone对象

public class PhoneJsonDeserializer extends JsonDeserializer<Phone> {
    @Override
    public Phone deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        String[] phonetArr = node.asText().split("-");
        Phone phone = new Phone();
        phone.setCode(phonetArr[0]);
        phone.setNumber(phonetArr[1]);
        return phone;
    }
}
  • @JsonComponent

如果使用Jackson对JSON数据进行序列化和反序列化,则可以编写自己的JsonSerializer和JsonDeserializer。然后通过@JsonSerialize和@JsonDeSerialize来指定具体类是否使用。

同时,SpringBoot提供了一个可选的@JsonComponent注释,可以将对应的JsonSerializer和JsonDeserializer直接注入为Spring Beans,从而实现全局化处理。

  • @JsonCreator
    @JsonCreator
    private Refresh(@JsonProperty("id") Long id) {
        this.id = id;
    }

当json在反序列化时,默认选择类的无参构造函数创建类对象,当没有无参构造函数时会报错,@JsonCreator作用就是指定反序列化时用的无参构造函数。构造方法的参数前面需要加上@JsonProperty,否则会报错。

  • @JsonManagedReference、@JsonBackReference

jackson中的@JsonBackReference和@JsonManagedReference,以及@JsonIgnore均是为了解决对象中存在双向引用导致的无限递归(infinite recursion)问题。这些标注均可用在属性或对应的get、set方法中。

@JsonBackReference和@JsonManagedReference:这两个标注通常配对使用,通常用在父子关系中。@JsonBackReference标注的属性在序列化(serialization,即将对象转换为json数据)时,会被忽略(即结果中的json数据不包含该属性的内容)。@JsonManagedReference标注的属性则会被序列化。在序列化时,@JsonBackReference的作用相当于@JsonIgnore,此时可以没有@JsonManagedReference。但在反序列化(deserialization,即json数据转换为对象)时,如果没有@JsonManagedReference,则不会自动注入@JsonBackReference标注的属性(被忽略的父或子);如果有@JsonManagedReference,则会自动注入自动注入@JsonBackReference标注的属性。

  • 代码片段
@Entity
@Table(name = "t_refresh")
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonIgnoreProperties(value = {"refreshTime"}, allowGetters = true, allowSetters = true)
public class Refresh implements Serializable {

    @JsonCreator
    private Refresh(@JsonProperty("id") Long id) {
        this.id = id;
    }

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    // @JsonValue
    private Long id;

    @JsonIgnore
    private String city;

    /**
     * 刷新,侦测时间点
     * mysql: datetime类型
     */
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy/MM/dd HH:mm:ss")
    /**
     * 默认就使用这个
     */
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime refreshTime;

    /**
     * @Temporal 持久化注解
     * mysql: 默认time类型
     */
    @JsonFormat(timezone = "GMT+8", pattern = "HH:mm:ss")
    private Date createDate;

    /**
     * @Temporal 持久化注解
     * mysql: 指定类型为TIMESTAMP
     * Date +  @Temporal == LocalDateTime
     */
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy/MM/dd HH:mm:ss")
    @Temporal(TemporalType.TIMESTAMP)
    private Date updateDate;

    @JsonFormat(timezone = "GMT+8", pattern = "yyyy/MM/dd")
    @Temporal(TemporalType.DATE)
    private Date hireDate;

    /**
     * 页码
     */
    @JsonAlias("myPage")
    private Integer page;

    /**
     * 第几行
     */
    @JsonProperty(value = "myRow", access = JsonProperty.Access.READ_ONLY)
    private Integer row;

    /**
     * 总排名(含广告)
     */
    private Integer score;

    private Boolean isMatched;

    private String path;


    public interface addressDetail{}
    public interface phoneDetail{}


    @Transient
    @JsonSerialize(using = AddressJsonSerializer.class)
    @JsonView(addressDetail.class)
    private Address address;

    @Transient
    @JsonDeserialize(using = PhoneJsonDeserializer.class)
    @JsonView(phoneDetail.class)
    private Phone phone;

    public void setScore(Integer score) {
        System.out.println(score);
        this.score = score;
    }
}

JPA注解

  • Transient

默认使用字段映射, 通过@Transient设置,不用数据库字段的映射。

  • @Temporal(TemporalType.TIME)

注意:@Temporal should only be set on a java.util.Date or java.util.Calendar property,不能用在LocalDateTime上,
Date + @Temporal(TemporalType.TIMESTAMP) == LocalDateTime。最佳实践,优先使用LocalDateTime

    {
        "refreshTime": "2020-11-17T09:59:27",  // LocalDateTime
        "updateDate": "2020-11-17T15:59:27.000+0000", // Date +  @Temporal(TemporalType.TIMESTAMP)
        "createDate": "1970-01-02T00:30:18.000+0000", // Date,默认TemporalType.TIME
        "hireDate": "2020-11-17", // Date + TemporalType.DATE
    }

  • @MappedSuperclass

基于代码复用和模型分离的思想,在项目开发中使用JPA的@MappedSuperclass注解将实体类的多个属性分别封装到不同的非实体类中。例如,数据库表中都需要id来表示编号,id是这些映射实体类的通用的属性,交给jpa统一生成主键id编号,那么使用一个父类来封装这些通用属性,并用@MappedSuperclas标识。

注意:

1.标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。

2.标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。

  • @PreUpdate、@PrePersist、@PreRemove

用于为相应的生命周期事件指定回调方法。

  • @Enumerated

使用此注解映射枚举字段,以String类型存入数据库

注入数据库的类型有两种:EnumType.ORDINAL(Interger)、EnumType.STRING(String)

  • @Embedded、@Embeddable

当一个实体类要在多个不同的实体类中进行使用,而其不需要生成数据库表。比如:address和user。只想生成一张表user,但是是两个类

  1. @Embeddable:注解在类上,表示此类是可以被其他类嵌套
  2. @Embedded:注解在属性上,表示嵌套被@Embeddable注解的同类型类
  • @CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy

表示字段为创建时间字段(insert自动设置)、创建用户字段(insert自动设置)、最后修改时间字段(update自定设置)、最后修改用户字段(update自定设置)

  用法:

    1、@EntityListeners(AuditingEntityListener.class):申明实体类并加注解

    2、@EnableJpaAuditing:在启动类中加此注解

    3、在实体类中属性中加上面四种注解

    4、自定义添加用户

import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@Configuration
public class UserIDAuditorBean implements AuditorAware<Long> {
    @Override
    public Long getCurrentAuditor() {
        SecurityContext ctx = SecurityContextHolder.getContext();
        if (ctx == null) {
            return null;
        }
        if (ctx.getAuthentication() == null) {
            return null;
        }
        if (ctx.getAuthentication().getPrincipal() == null) {
            return null;
        }
        Object principal = ctx.getAuthentication().getPrincipal();
        if (principal.getClass().isAssignableFrom(Long.class)) {
            return (Long) principal;
        } else {
            return null;
        }
    }
}
  • @DynamicInsert

@DynamicInsert属性:设置为true,表示insert对象的时候,生成动态的insert语句,如果这个字段的值是null就不会加入到insert语句中,默认false。比如希望数据库插入日期或时间戳字段时,在对象字段为空定的情况下,表字段能自动填写当前的sysdate

  • @DynamicUpdate

@DynamicUpdate属性:设置为true,表示update对象的时候,生成动态的update语句,如果这个字段的值是null就不会被加入到update语句中,默认false。
比如只想更新某个属性,但是却把整个属性都更改了,这并不是我们希望的结果,我们希望的结果是:我更改了哪写字段,只要更新我修改的字段就够了

  • AttributeConverter

实体属性类型转换器。更多信息参考https://xhope.top/?p=1122

  • 小技巧

一对多,由多的一方维护关联关系时,如公司group和部门departments。前端传递JSON,可以这么传:

{
    "id": 5
    "name": 'Google',
    "departmentIds": [1, 2]
}

后端:

    @Transient
    private Long[] departmentIds;

    public void setDepartmentIds(Long[] departmentIds) {
        this.departmentIds = departmentIds;

        // 反序列化的时候,进行处理
        if (Objects.nonNull(departmentIds) && departmentIds.length > 0) {
            departments = new ArrayList<>(departmentIds.length);
            for (Long departmentId : departmentIds) {
                departments.add(new Department(departmentId));
            }
        }
    }

JPA关联关系

一对多@OneToMany

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OneToMany {
    Class targetEntity() default void.class;

    CascadeType[] cascade() default {};

    FetchType fetch() default FetchType.LAZY;

    String mappedBy() default "";

    boolean orphanRemoval() default false;
}

用在集合属性上:如List表示有序,Set表示不重复

Group.java

    @OneToMany(cascade = CascadeType.PERSIST, orphanRemoval= true, mappedBy = "group")
    private List<Department> departments;

“一对多”或“多对一”,需要在多的一方加外键。通过注解@JoinColumn完成

    @JoinColumn(name = "group_id")

这个既可以加在“多”的一方,也可以加在“一”的一方。
1. 加在“一”的一方表示由“一”的一方维护。
2. 加在“多”的一方表示由多的一方维护。“一”多一方必须使用mappedBy交出控制。否则可能会创建表(字段)

mappedBy:

  1. 只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性;
  2. mappedBy标签一定是定义在被拥有方的,他指向拥有方;
  3. mappedBy的含义,应该理解为,拥有方能够自动维护跟被拥有方的关系,当然,如果从被拥有方,通过手工强行来维护拥有方的关系也是可以做到的;
  4. mappedBy跟joinColumn/JoinTable总是处于互斥的一方,可以理解为正是由于拥有方的关联被拥有方的字段存在,拥有方才拥有了被拥有方。mappedBy这方定义JoinColumn/JoinTable总是失效的,不会建立对应的字段或者表。

mappedBy表示声明自己不是一对多的关系维护端,由对方来维护,是在一的一方进行声明的。mappedBy的值应该为一的一方的表名。

orphanRemoval:

  1. jpa 中 orphanRemoval 属性,如果为 true 的话,想要删掉子集合数据,那么调用子集合list 的 clear 方法清空,并且断关系可以直接在数据库中删除子集合数据, 不能直接设置 为null,否则抛出异常.
  2. 如果没有该属性,调用子集合list 的 clear 方法清空,并且断关系则在数据库中把 子表数据中保存的主表id 设置为空,断开关系;
  3. 而cascade 是总开关,如果 这里没有设置 CascadeType.all 或者 delete ,那么就算 orphanRemoveal 设置为 true 也无法执行删除.

FetchType:

FetchType.LAZY:懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载

FetchType.EAGER:急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载

CascadeType

CascadeType.MERGE级联更新:若items属性修改了那么order对象保存时同时修改items里的对象。对应EntityManager的merge方法 (较常用 )

CascadeType.PERSIST级联保存:对order对象保存时也对items里的对象也会保存。对应EntityManager的presist方法

CascadeType.REFRESH级联刷新:获取order对象里也同时也重新获取最新的items时的对象。对应EntityManager的refresh(object)方法有效。即会重新查询数据库里的最新数据

CascadeType.REMOVE级联删除:对order对象删除也对items里的对象也会删除。对应EntityManager的remove方法

CascadeType.ALL包含所有;

多对一@ManyToOne

public @interface ManyToOne {
    Class targetEntity() default void.class;

    CascadeType[] cascade() default {};

    FetchType fetch() default FetchType.EAGER;

    boolean optional() default true;
}

optional
同表示是否允许为空。比如公司和部门。不允许没有公司的部门,所以需要设置optional = false

    Group group = new Group();
    group.setId(1L);

    Department department = new Department();
    department.setName("111");
    department.setGroup(group); //  @ManyToOne(optional = false) 必须设置group
    departmentRepository.save(department);

一对一

多对多@ManyToMany

“多对多”通过JoinTable增加一张中间表,跟@JoinColumn增加一个外键字段的作用一致的,提供“连接”。

public @interface ManyToMany {
    Class targetEntity() default void.class;

    CascadeType[] cascade() default {};

    FetchType fetch() default FetchType.LAZY;

    String mappedBy() default "";
}
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "t_user_role", 
    joinColumns = { @JoinColumn(name = "user_id") },
    inverseJoinColumns = { @JoinColumn(name = "role_id") })
    public Set<Role> getRoles() {
        return roles;
    }

这样就会创建一张中间表t_user_role,分别有两个字段user_idrole_id,分别指向user和role表。

如果想在中间表中,添加自己的「id」, 「创建时间」等。@ManyToMany就不能实现了。

可以自己通过创建中间表UserRole

User 1 <-> N UserRole
Role 1 <-> N UserRole

User.java

@Data
@Entity
@Table(name = "t_user")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "user_id")
    private List<UserRole> roleList;
}

Role.java

@Data
@Entity
@Table(name = "t_role")
public class Role implements Serializable {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String name;
}

UserRole.java

@Data
@Entity
@Table(name = "t_user_role")
public class UserRole {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private LocalDateTime createdDate;

    @ManyToOne
    private User user;

    @ManyToOne
    @JoinColumn(name = "role_id")
    private Role role;

}

Test.java

    Role role = new Role();
    role.setName("admin1");
    roleRepository.save(role); // 保存角色

    User user = new User();
    user.setName("Rick1");


    UserRole userRole = new UserRole();
    userRole.setCreatedDate(LocalDateTime.now());
    userRole.setRole(role); // 绑定角色关联关系

    user.setRoleList(Lists.newArrayList(userRole)); // 绑定用户跟关系表
    userRole.setRole(role);

    userRepository.save(user);

事务处理

通过spring注解@Transactional实现。 org.springframework.transaction.annotation.Transactional,这个注解对JPA操作和JdbcTemplate,都能统一管理事务,也就是说可以在一个service中同时使用JdbcTemplate操作,又可以使用JPA操作。

    @Test
    @Transactional
    public void testAddTransactional() {
        Department department = new Department();
        department.setName("IT");
        departmentRepository.save(department);

        int a = 5/0; // 抛出异常并回滚

        Role role = new Role();
        role.setName("admin");
        roleRepository.save(role);

    }
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}
  • 0、如果调用方法加@Transactional,嵌套调用的方法,发生Runtime的异常,也会回滚,即使嵌套调用的方法没加@Transactional;反之,嵌套调用的方法加注解@Transactional,发生异常,但调用方法没有加注解,那么,不回滚。??TODO,需要在深入理解下“spring的事务传播行为”,如果嵌套但方法是当前对象的方法,this是当前被代理的对象,已经不是spring自动代理的对象了。之前做cache的时候,也遇到类似的情况。Spring事务管理
  • 1、异常在A方法内抛出,则A方法就得加注解
  • 2、多个方法嵌套调用,如果都有@Transactional 注解,则产生事务传递,默认 Propagation.REQUIRED
  • 3、如果注解上只写 @Transactional 默认只对 RuntimeException 回滚,而非 Exception 进行回滚
  • 如果要对 checked Exceptions 进行回滚,则需要 @Transactional(rollbackFor = Exception.class)

更多参考
* https://www.cnblogs.com/taven/p/5942384.html
* https://www.cnblogs.com/flydean/p/12680284.html

Vue生命周期执行测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="root">
        <div>{{fullName}}</div>
        <button @click="change">change firstName</button>
    </div>


    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.9/vue.min.js"></script>
    <script>
        new Vue({
            el: '#root',
            data: {
                firstName: 'Rick',
                lastName: 'Xu'
            },
            computed: {
                /*
                    使用的时候才会执行: 1. 页面绑定, 2 代码调用
                    不要再computed,对data属性的值进行修改
                */
                fullName: function() {
                    let fullName = this.firstName + ' ' + this.lastName
                    console.log('-  computed ', fullName)
                    return fullName
                }
            },
            watch: {
                /*
                    整个生命周期,只会出发一次监控
                */
                firstName: function(val, old) {
                    console.log('-  watch ', old, ' => ', val)
                }
            },
            created() {
                /*
                    // 还未完成页面渲染
                    ajax请求放在created中,比在mounted好

                    1. 会调用computed
                    2. 因为mounted也对「firstName」进行了修改,「watch」mounted后被调用一次。created不调用
                */

                // console.log(this.fullName) // Rick.Xu 手动调用
                this.firstName = 'Jim'
                console.log('1. created')
                // 1. 页面绑定了computed属性,会调用一次

            },
            mounted() {
                /*
                    完成页面渲染
                    会调用watch和computed方法
                    操作dom,jquery事件绑定在这里处理
                */
                this.firstName = 'Ashley'
                console.log('2. mounted')
            },
            updated() {
                console.log('3. updated')
            },
            methods: {
                change: function() {
                    /*
                        先执行watch,再computed
                    */
                    this.firstName = 'Lucy'
                    console.log('4. change')
                }
            }
        })

    </script>
</body>
</html>

http://xhope.top/wp-content/uploads/2020/11/qq.png

js中,Tree树形控件数据由「数组格式」转换成「对象格式」

数组格式
数据库中通过pId维护关系

[{
    "id": 1576757423116692,
    "name": "\u57FA\u672C\u4FE1\u606F",
    "open": true,
    "icon": null,
    "iconSkin": null,
    "pId": null // 通过pId标示父节点
}, {
    "id": 1576830102194814,
    "name": "\u6D88\u606F\u67E5\u770B",
    "open": true,
    "icon": null,
    "iconSkin": null,
    "pId": 1576757423116692
}]

对象格式
Tree树形控件,通过children维护节点关系

[{
    "id": 1592539382001818,
    "name": "报表中心",
    "open": true,
    "icon": null,
    "iconSkin": null,
    "pId": null,
    "children": [{ // children嵌套
        "id": 1592539382041842,
        "name": "库存分析",
        "open": true,
        "icon": null,
        "iconSkin": null,
        "pId": 1592539382001818
    }]
}]

js中,Tree树形控件数据由「数组格式」转换成「对象格式」

 var permissionList = [{"id":1576757423116692,"name":"\u57FA\u672C\u4FE1\u606F","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576830102194814,"name":"\u6D88\u606F\u67E5\u770B","open":true,"icon":null,"iconSkin":null,"pId":1576830101976741},{"id":1576738932216633,"name":"\u7269\u6599\u54C1\u724C","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1576830009811502,"name":"\u6837\u54C1\u7533\u8BF7","open":true,"icon":null,"iconSkin":null,"pId":1576830009617716},{"id":1576830518754858,"name":"\u8BBF\u95EE\u65F6\u95F4\u63A7\u5236","open":true,"icon":null,"iconSkin":null,"pId":1576830518507293},{"id":1576757422730815,"name":"\u7269\u6599\u4FE1\u606F","open":true,"icon":null,"iconSkin":null,"pId":null},{"id":1576900467172242,"name":"DICETYPE","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576830331048506,"name":"\u7269\u6599\u5206\u7C7B","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1592539382001422,"name":"\u51FA\u8D27\u7EC6\u5316-\u5BA2\u6237\u7EDF\u8BA1","open":true,"icon":null,"iconSkin":null,"pId":1592539382001821},{"id":1576830009915354,"name":"\u4EF7\u683C\u7533\u8BF7","open":true,"icon":null,"iconSkin":null,"pId":1576830009617716},{"id":1576830102324678,"name":"\u53D1\u5E03\uFF0F\u7F16\u8F91\u6D88\u606F","open":true,"icon":null,"iconSkin":null,"pId":1576830101976741},{"id":1576830009617716,"name":"\u7533\u8BF7","open":true,"icon":null,"iconSkin":null,"pId":null},{"id":1576830518754859,"name":"\u6B63\u80FD\u91CF\u8D26\u53F7\u8BBE\u7F6E","open":true,"icon":null,"iconSkin":null,"pId":1576830518507293},{"id":1576830331048502,"name":"\u7269\u6599\u578B\u53F7","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1592539382041842,"name":"\u5E93\u5B58\u5206\u6790","open":true,"icon":null,"iconSkin":null,"pId":1592539382001818},{"id":1576830101976741,"name":"\u6D88\u606F\u4E2D\u5FC3","open":true,"icon":null,"iconSkin":null,"pId":null},{"id":1576830518754260,"name":"\u56FD\u5185\u4ED3\u9501\u8D27\u6743\u9650","open":true,"icon":null,"iconSkin":null,"pId":1576830518507293},{"id":1592539382001842,"name":"\u51FA\u8D27\u62A5\u8868","open":true,"icon":null,"iconSkin":null,"pId":1592539382001818},{"id":1592539382005822,"name":"\u51FA\u8D27\u7EC6\u5316-\u51FA\u8D27\u5360\u6BD4","open":true,"icon":null,"iconSkin":null,"pId":1592539382001821},{"id":1576830331258625,"name":"\u7269\u6599\u56FD\u5185\u5E93\u5B58","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1576830102433969,"name":"\u5220\u9664\u6D88\u606F","open":true,"icon":null,"iconSkin":null,"pId":1576830101976741},{"id":1576830010022199,"name":"\u5F00\u53D1\u5DE5\u5177\u7533\u8BF7","open":true,"icon":null,"iconSkin":null,"pId":1576830009617716},{"id":1576829852480747,"name":"\u5B98\u65B9\u4EA4\u671F","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576830331474734,"name":"\u672C\u5E74\u5EA6\u51FA\u8D27\u6570\u91CF","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1592539382001821,"name":"\u51FA\u8D27\u7EC6\u5316","open":true,"icon":null,"iconSkin":null,"pId":1592539382001818},{"id":1577173031332586,"name":"\u5E93\u5B58","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576830518754261,"name":"\u4F9B\u5E94\u94FE\u0026\u5E93\u5B58\u9884\u8B66\u6BD4\u4F8B","open":true,"icon":null,"iconSkin":null,"pId":1576830518507293},{"id":1592539382001818,"name":"\u62A5\u8868\u4E2D\u5FC3","open":true,"icon":null,"iconSkin":null,"pId":null},{"id":1576830160797318,"name":"\u4EF7\u683C\u8BBE\u7F6E","open":true,"icon":null,"iconSkin":null,"pId":null},{"id":1576829852861737,"name":"\u8FDB\u4EF7","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576830331363807,"name":"\u4E0A\u5E74\u5EA6\u51FA\u8D27\u6570\u91CF","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1576830334048506,"name":"\u5E02\u573A\u65B9\u5411","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1576738931829860,"name":"\u6570\u636E\u540C\u6B65","open":true,"icon":null,"iconSkin":null,"pId":null},{"id":1576830331258621,"name":"\u9999\u6E2F\u4ED3","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1576830397532753,"name":"\u6743\u9650\u7BA1\u7406","open":true,"icon":null,"iconSkin":null,"pId":null},{"id":1576830331258622,"name":"\u9999\u6E2F\u76F4\u8FD0\u4ED3","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1577157938152502,"name":"\u62A5\u4EF7","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576830331258623,"name":"\u5728\u9014\u5E93\u5B58\u6570\u91CF","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1576830518507293,"name":"\u7CFB\u7EDF\u8BBE\u7F6E","open":true,"icon":null,"iconSkin":null,"pId":null},{"id":1577173031603937,"name":"\u62A5\u4EF7\u53C2\u8003","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576830331258620,"name":"\u4F9B\u5E94\u94FE\u672A\u5B8C\u6210\u6570\u91CF","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1577157937807987,"name":"\u5F00\u53D1\u5DE5\u5177","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576830331258633,"name":"\u5BA2\u6237\u7AEF\u672A\u5B8C\u6210\u6570\u91CF","open":true,"icon":null,"iconSkin":null,"pId":1576738931829860},{"id":1576829852988791,"name":"\u5386\u53F2\u51FA\u8D27\u6570\u91CF","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576757423116672,"name":"\u7F16\u8F91\u4E3B\u63A8\u578B\u53F7","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576757423116673,"name":"\u7F16\u8F91\u89C6\u9891","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815},{"id":1576757423116675,"name":"\u6D4F\u89C8\u89C6\u9891","open":true,"icon":null,"iconSkin":null,"pId":1576757422730815}]

function parse(data) {
    let children = ret(data, null)

    function ret(_data, pid) {
        let _children = []
        let otherNode = []
        _data.forEach(node => {
            if (node.pId === pid) {
                _children.push(node)
            } else {
                otherNode.push(node)
            }
        })

        if (_children.length > 0) {
            // 排序
            _children = _children.sort(sort)

            _children.forEach(_node => {
                let node_child = ret(otherNode, _node.id)
                if (node_child.length > 0) {
                    _node.children = node_child
                }

            })   
        }
        return _children
    }

    function sort(a, b) { // 自定义排序规则
        return b.id - a.id
    }

    return children    
}

let formatData = parse(permissionList)

console.log(JSON.stringify(formatData))

converters chain

Request data
=> HttpMessageConverter
=> FormHttpMessageConverter
=> MappingJackson2HttpMessageConverter && @RequestBody
=> Yml2HttpMessageConverter(自定义)
=> Validation验证
=> 数据持久化
=> 返回json

数据流转

HttpMessageConverter

采用RequestResponseBodyMethodProcessor方法执行器,
前台提交数据,设置contentType,spring-web根据contentType,解析数据,并映射。

spring内置当HttpMessageConverter有

  • ByteArrayHttpMessageConverter – converts byte arrays
  • StringHttpMessageConverter – converts Strings
  • ResourceHttpMessageConverter – converts org.springframework.core.io.Resource for any type of octet stream
  • SourceHttpMessageConverter – converts javax.xml.transform.Source
  • FormHttpMessageConverter – converts form data to/from a MultiValueMap<String, String>.
  • Jaxb2RootElementHttpMessageConverter – converts Java objects to/from XML (added only if JAXB2 is present on the classpath)
  • MappingJackson2HttpMessageConverter – converts JSON (added only if Jackson 2 is present on the classpath)
  • MappingJacksonHttpMessageConverter – converts JSON (added only if Jackson is present on the classpath)
  • AtomFeedHttpMessageConverter – converts Atom feeds (added only if Rome is present on the classpath)
  • RssChannelHttpMessageConverter – converts RSS feeds (added only if Rome is present on the classpath)

平时我们用当最多当就是MappingJackson2HttpMessageConverterFormHttpMessageConverter

  • FormHttpMessageConverter使用的contentTypeapplication/x-www-form-urlencodedmultipart/form-data
  • MappingJackson2HttpMessageConverter使用的contentTypeapplication/json

NOTED

如果使用GET请求数据,那么使用ServletModelAttributeMethodProcessor,将不会使用转化器类。

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

类型转换

  • MappingJackson2HttpMessageConverter对基本类型(包括枚举,List<> String[])支持都非常好。如果自定义类需要继承JsonDeserializer
@JsonComponent
public class PhoneJsonDeserializer extends JsonDeserializer<Phone> {
    @Override
    public Phone deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        String[] phonetArr = node.asText().split("-");
        Phone phone = new Phone();
        phone.setCode(phonetArr[0]);
        phone.setNumber(phonetArr[1]);
        return phone;
    }
}
  • GET提交和multipart/form-data实现Converter
@Component
public class PhoneConverter implements Converter<String, Phone> {

    @Override
    public Phone convert(String source) {
        if (StringUtils.isEmpty(source)) {
            return null;
        }

        String[] phonetArr = source.split("-");
        Phone phone = new Phone();
        phone.setCode(phonetArr[0]);
        phone.setNumber(phonetArr[1]);

        return phone;
    }
}

自定义转换器

定义一个可以转换application/yml的转换器
pom.xml添加

 <dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.19</version>
</dependency>
@Component
public class Yml2HttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    public Yml2HttpMessageConverter() {
        super(new MediaType[]{new MediaType("application", "yml"), new MediaType("application", "ymal")});
    }

    @Override
    protected boolean supports(Class<?> aClass) {
        return true;
    }

    @Override
    protected Object readInternal(Class<?> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
        Yaml yaml = new Yaml();
        return yaml.loadAs(httpInputMessage.getBody(), aClass);
    }

    @Override
    protected void writeInternal(Object o, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
        Yaml yaml = new Yaml();
        OutputStream outputStream = httpOutputMessage.getBody();
        outputStream.write(yaml.dump(o).getBytes());
        outputStream.close();
    }
}

@Component注解会放在在所有converter的第一个。优先于MappingJackson2HttpMessageConverter。要改变顺序,WebMvcConfigurer中去实现方法extendMessageConverters。导致response的时候,都是yml格式,而不是json格式。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * https://www.cnblogs.com/slankka/p/11437034.html
     * 加到最后
     * @param converters
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new Yml2HttpMessageConverter());
    }
}

这样response的时候,优先json格式,如果需要yml格式的返回可以手动指定

@GetMapping (produces = "application/yml")
public UserDTO getData(@RequestBody UserDTO userDTO) {
    log.info(userDTO.toString());
    return userDTO;
}

Validation

使用默认验证器

controller添加注解@Valid,model添加验证注解如:@NotBlank

    @GetMapping (produces = "application/yml")
    public UserDTO getData(@RequestBody @Valid UserDTO userDTO, BindingResult result) throws Exception {
        if (result.hasErrors()) {
            throw new Exception("验证失败");
        }
        log.info(userDTO.toString());
        return userDTO;
    }

自定义验证器

@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumberConstraint {
    String message() default "Invalid phone number";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
public class PhoneNumberValidator implements
        ConstraintValidator<PhoneNumberConstraint, Phone> {

    @Override
    public void initialize(PhoneNumberConstraint constraintAnnotation) {
    }

    /**
     * @param phone
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Phone phone, ConstraintValidatorContext context) {
        if (Objects.isNull(phone)) {
            return true;
        }

        return (phone.getCode() + "-" + phone.getNumber()).matches("\\d+[-]\\d+");
    }
}

使用注解

 @PhoneNumberConstraint
 private Phone phone;

持久化

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.9</version>
    </dependency>

application.xml

spring:
  datasource:
    name: druidDataSource
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/iis?useUnicode=true&characterEncoding=UTF-8
      username: root
      password: jkxyx205
      initialSize: 1
      minIdle: 5
      maxActive: 10
      maxWait: 60000
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

    hibernate:
      ddl-auto: update
public class ListJsonAttributeConverter<T> implements AttributeConverter<List<T>, String> {

    @Override
    public String convertToDatabaseColumn(List<T> list) {
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }

        try {
            return JsonUtils.toJson(list);
        } catch (IOException e) {
            return null;
        }
    }

    @Override
    public List<T> convertToEntityAttribute(String json) {
        Class<?> clazz = ClassUtils.getActualTypeArgument(this.getClass())[0];

        if (StringUtils.isBlank(json)) {
            return Collections.emptyList();
        }

        try {
            return (List<T>) JsonUtils.toList(json, clazz);
        } catch (IOException e) {
            e.printStackTrace();
            return Collections.emptyList();
        }
    }
}
public class PojoJsonAttributeConverter<T> implements AttributeConverter<T, String> {

    @Override
    public String convertToDatabaseColumn(T t) {
        if (Objects.isNull(t)) {
            return null;
        }

        try {
            return JsonUtils.toJson(t);
        } catch (IOException e) {
            return null;
        }
    }

    @Override
    public T convertToEntityAttribute(String json) {
        Class<?> clazz = ClassUtils.getActualTypeArgument(this.getClass())[0];
        if (StringUtils.isBlank(json)) {
            return null;
        }

        try {
            return (T) JsonUtils.toObject(json, clazz);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

将对象以 JSON 字符串的形式持久化到数据库中,使用继承指定范型类型

public class PriceConverter extends ListJsonAttributeConverter<List<QuotationExpression>> {}
public class OrderConverter extends PojoJsonAttributeConverter<QuotationExpression> {}

指定对应的convert

@Convert(converter = PriceConverter.class)
private List<QuotationExpression> quotationExpressionList;

@Convert(converter =OrderConverter.class)
private QuotationExpression quotationExpression;

https://vladmihalcea.com/jpa-attributeconverter/

https://zenidas.wordpress.com/recipes/jpa-converter-as-json/

response JSON

public class AddressJsonSerializer extends JsonSerializer<Address> {

    @Override
    public void serialize(Address address, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        address.setAddrDetail("China "+ address.getAddrDetail());
        jsonGenerator.writeObject(address);
    }
}
    @Convert(converter = AddressConverter.class)
    @JsonSerialize(using = AddressJsonSerializer.class)
    private Address address;

Spring Security + Spring Security Oauth2

Spring Security

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf对security的支持-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>${thymeleaf-extras-springsecurity5-version}</version>
</dependency>

Web端:

WebSecurityConfig

主要端配置类:WebSecurityConfig,在这个文件中可以配置以下

  • loginPage 登录页面的地址(如果没有提供,Spring Security提供了默认的登录界面)

  • handler:

    • successHandler
    • authenticationFailureUrl
    • logoutSuccessHandler
  • session处理相关配置
  • 授权管理
  • 配置新的拦截器

WebSecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled=true, prePostEnabled = true)// 控制权限注解 配合 @Secured({"ROLE_ADMIN","ROLE_USER2"})使用
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ValidateFilter validateFilter;

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().disable()
                .authorizeRequests()
                .antMatchers("/forbidden", "/kaptcha").permitAll()
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .permitAll()
                .and()
                .logout()
                .logoutSuccessHandler(logoutSuccessHandler)
                .permitAll()
                .and()
                .csrf().disable()// csrf会拦截POST请求 https://www.jianshu.com/p/2c275c75c77a
                .sessionManagement().
                    invalidSessionUrl("/login")
                    .maximumSessions(1)
                    .expiredSessionStrategy(new AcExpiredSessionStrategy())
                    .sessionRegistry(sessionRegistry());


        http.addFilterBefore(validateFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/coreui/**", "/css/**", "/js/**", "/img/**", "/plugins/**", "/favicon.ico");
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
}

PasswordEncoder

密码加密处理
pom.xml

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

用户认证服务UserDetailsService

@Component
@Slf4j
public class AcUserDetailsService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Autowired
    private CacheManager cacheManager;

    @Autowired
    private SessionRegistry sessionRegistry;


    /**
     * 进行认证授权的工作
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) {
        username = username.toLowerCase();
        Cache tryCache = cacheManager.getCache("loginMaxTry");

        Object object = tryCache.get(username);
        int loginMaxTryCount = 1;
        if (Objects.isNull(object)) {
            tryCache.put(username, loginMaxTryCount);
        } else {
            loginMaxTryCount = (Integer) object + 1;
            tryCache.put(username, loginMaxTryCount);
        }

        if (loginMaxTryCount > AuthConstants.MAX_TRY_COUNT) {
            throw new MaxTryLoginException();
        }

        UserDTO user;

        try {
            user = userService.findByUsername(username);
        } catch (DataAccessException e) {
            throw new DataAccessResourceFailureException("数据库连接出错, 请联系管理员");
        }

        if (user == null) {
            throw new UsernameNotFoundException("帐号不存在或者账号被锁定");
        }

        if (isLogin(username)) {
            log.info("LOGIN: {}已经登录,踢出登录", username );
        }

        return new AcUserDetails(user, AuthorityUtils.createAuthorityList(user.getRoles().toArray(new String[]{})));
    }

    /**
     * 用户是否处于登录状态
     * @param username
     * @return
     */
    private boolean isLogin(String username) {
        List<Object> list = sessionRegistry.getAllPrincipals();
        for (Object o : list) {
            if (StringUtils.equals(((AcUserDetails)o).getUsername(), username)) {
                return sessionRegistry.getAllSessions(o, false).size() > 0;
            }

        }

        return false;
    }
}

Spring Security OAuth2

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

在Spring Security的基础上多次两个概念
* @EnableAuthorizationServer 授权服务器
* @EnableResourceServer 资源服务器

如果授权服务器和资源服务器部署「在」同一台服务器上

  • 采用授权码模式会有问题,因为采用授权码模式需要跳转「登录」界面,需要使用WebSecurityConfigWebSecurityConfig@EnableResourceServer会有冲突。

  • 采用密码模式没有问题。不需要依赖WebSecurityConfig,因为不依赖WebSecurityConfig,需要自己构造AuthenticationManager,测试时否则会报异常。


@Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { List<AuthenticationProvider> providers = new ArrayList<>(); DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); providers.add(daoAuthenticationProvider); return new ProviderManager(providers); }

如果授权服务器和资源服务器部署「不在」同一台服务器上

  • 授权服务器

    • WebSecurityConfig + @EnableAuthorizationServer 授权码和密码模式
    • @EnableAuthorizationServer 密码模式
  • 资源服务器
    @EnableResourceServer

授权服务器

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    @Qualifier("tokenServices")
    private AuthorizationServerTokenServices tokenServices;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
                .checkTokenAccess("permitAll()")//oauth/check_token公开
                .allowFormAuthenticationForClients()                //密码模式:表单认证(申请令牌)
        ;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                clients.inMemory()// 使用in-memory存储
                .withClient("c1")// client_id
                .secret(passwordEncoder.encode("secret"))//客户端密钥
                .resourceIds("res1")//资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //加上验证回调地址
                .redirectUris("http://www.baidu.com");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)// 认证管理器 => 密码模式需要在认证服务器中设置 中配置AuthenticationManager
                .authorizationCodeServices(authorizationCodeServices)//授权码服务
                .tokenServices(tokenServices)// 令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }


资源服务器

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = "res1";

    @Autowired
    private TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 资源id
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore)
                .stateless(true)
                .accessDeniedHandler((request, response, e) -> {
                    response.getWriter().write("look=>" + e);
                });
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/order/r2").hasAnyAuthority("p2")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                // 需要身份认证的时候跳转的URL(可以直接指定html)
        ;
    }
}