• Spring Boot JPA - Querydsl

    Querydsl 是一个类型安全的 Java 查询框架,支持 JPA, JDO, JDBC, Lucene, Hibernate Search 等标准。类型安全(Type safety)和一致性(Consistency)是它设计的两大准则。在 Spring Boot 中可以很好的弥补 JPA 的不灵活,实现更强大的逻辑。

    依赖

    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <scope>provided</scope>
    </dependency>
    
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
    </dependency>
    

    因为是类型安全的,所以还需要加上Maven APT plugin,使用 APT 自动生成一些类:

    <project>
      <build>
      <plugins>
        ...
        <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
            <execution>
                <phase>generate-sources</phase>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
    </plugin>
        ...
      </plugins>
      </build>
    </project>
    

    基本概念

    每一个 Model (使用 @javax.persistence.Entity 注解的),Querydsl 都会在同一个包下生成一个以 Q 开头(默认,可配置)的类,来实现便利的查询操作。 如:

    JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
    // 基本查询
    List<Person> persons = queryFactory.selectFrom(person)
      .where(
        person.firstName.eq("John"),
        person.lastName.eq("Doe"))
      .fetch();
    
    // 排序
    List<Person> persons = queryFactory.selectFrom(person)
      .orderBy(person.lastName.asc(), 
               person.firstName.desc())
      .fetch();
    
    // 子查询
    List<Person> persons = queryFactory.selectFrom(person)
      .where(person.children.size().eq(
        JPAExpressions.select(parent.children.size().max())
                      .from(parent)))
      .fetch();
    
    // 投影
    List<Tuple> tuples = queryFactory.select(
        person.lastName, person.firstName, person.yearOfBirth)
      .from(person)
      .fetch();
    

    有没有很强大? 。。。。。 来看一个具体一点的例子吧:

    实例

    接着上一章的几个表,来一点高级的查询操作:

    Spring 提供了一个便捷的方式使用 Querydsl,只需要在 Repository 中继承 QueryDslPredicateExecutor 即可:

    @Repository
    public interface UserRepository extends JpaRepository<User, Long>, QueryDslPredicateExecutor<User> {
    }
    

    然后就可以使用 UserRepository 无缝和 Querydsl 连接:

    userRepository.findAll(user.name.eq("lufifcc")); // 所有用户名为 lufifcc 的用户
    
    userRepository.findAll(
            user.email.endsWith("@gmail.com")
                    .and(user.name.startsWith("lu"))
                    .and(user.id.in(Arrays.asList(520L, 1314L, 1L, 2L, 12L)))
    ); // 所有 Gmail 用户,且名字以 lu 开始,并且 ID 在520L, 1314L, 1L, 2L, 12L中
    
    userRepository.count(
            user.email.endsWith("@outlook.com")
                    .and(user.name.containsIgnoreCase("a"))
    ); // Outlook 用户,且名字以包含 a (不区分大小写)的用户数量
    
    userRepository.findAll(
            user.email.endsWith("@outlook.com")
                    .and(user.posts.size().goe(5))
    ); // Outlook 用户,且文章数大于等于5
    
    userRepository.findAll(
            user.posts.size().eq(JPAExpressions.select(user.posts.size().max()).from(user))
    );// 文章数量最多的用户
    

    另外, Querydsl 还可以采用 SQL 模式查询,加入依赖:

    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-sql</artifactId>
    </dependency>
    

    1. 然后,获取所有用户邮箱:

    @GetMapping("users/emails")
    public Object userEmails() {
        QUser user = QUser.user;
        return queryFactory.selectFrom(user)
                .select(user.email)
                .fetch();
    }
    // 返回
    [
      "[email protected]",
      "[email protected]",
      "[email protected]",
      "[email protected]"
    ]
    

    2. 获取所有用户名和邮箱:

    @GetMapping("users/names-emails")
    public Object userNamesEmails() {
        QUser user = QUser.user;
        return queryFactory.selectFrom(user)
                .select(user.email, user.name)
                .fetch()
                .stream()
                .map(tuple -> {
                    Map<String, String> map = new LinkedHashMap<>();
                    map.put("name", tuple.get(user.name));
                    map.put("email", tuple.get(user.email));
                    return map;
                }).collect(Collectors.toList());
    }
    // 返回
    [
      {
        "name": "Lufficc",
        "email": "[email protected]"
      },
      {
        "name": "Allen",
        "email": "[email protected]"
      },
      {
        "name": "Mike",
        "email": "[email protected]"
      },
      {
        "name": "Lucy",
        "email": "[email protected]"
      }
    ]
    

    注意到投影的时候,我们可以直接利用查询时用到的表达式来获取类型安全的值,如获取 name : tuple.get(user.name),返回值是 String 类型的。

    3. 获取所有用户ID,名称,和他们的文章数量:

    @GetMapping("users/posts-count")
    public Object postCount() {
        QUser user = QUser.user;
        QPost post = QPost.post;
        return queryFactory.selectFrom(user)
                .leftJoin(user.posts, post)
                .select(user.id, user.name, post.count())
                .groupBy(user.id)
                .fetch()
                .stream()
                .map(tuple -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("id", tuple.get(user.id));
                    map.put("name", tuple.get(user.name));
                    map.put("posts_count", tuple.get(post.count()));
                    return map;
                }).collect(Collectors.toList());
    }
    // 返回
    [
      {
        "id": 1,
        "name": "Lufficc",
        "posts_count": 9
      },
      {
        "id": 2,
        "name": "Allen",
        "posts_count": 6
      },
      {
        "id": 3,
        "name": "Mike",
        "posts_count": 6
      },
      {
        "id": 4,
        "name": "Lucy",
        "posts_count": 6
      }
    ]
    

    4. 获取所有用户名,以及对应用户的 JavaPython 分类下的文章数量:

    @GetMapping("users/category-count")
    public Object postCategoryMax() {
        QUser user = QUser.user;
        QPost post = QPost.post;
        NumberExpression<Integer> java = post.category
                .name.lower().when("java").then(1).otherwise(0);
        NumberExpression<Integer> python = post.category
                .name.lower().when("python").then(1).otherwise(0);
        return queryFactory.selectFrom(user)
                .leftJoin(user.posts, post)
                .select(user.name, user.id, java.sum(), python.sum(), post.count())
                .groupBy(user.id)
                .orderBy(user.name.desc())
                .fetch()
                .stream()
                .map(tuple -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("username", tuple.get(user.name));
                    map.put("java_count", tuple.get(java.sum()));
                    map.put("python_count", tuple.get(python.sum()));
                    map.put("total_count", tuple.get(post.count()));
                    return map;
                }).collect(Collectors.toList());
    }
    
    // 返回
    [
      {
        "username": "Mike",
        "java_count": 3,
        "python_count": 1,
        "total_count": 5
      },
      {
        "username": "Lufficc",
        "java_count": 2,
        "python_count": 4,
        "total_count": 9
      },
      {
        "username": "Lucy",
        "java_count": 1,
        "python_count": 1,
        "total_count": 5
      },
      {
        "username": "Allen",
        "java_count": 2,
        "python_count": 1,
        "total_count": 5
      }
    ]
    

    注意这里用到了强大的 Case 表达式(Case expressions)

    5. 统计每一年发文章数量,包括 JavaPython 分类下的文章数量:

    @GetMapping("posts-summary")
    public Object postsSummary() {
        QPost post = QPost.post;
        ComparableExpressionBase<?> postTimePeriodsExp = post.createdAt.year();
        NumberExpression<Integer> java = post.category
                .name.lower().when("java").then(1).otherwise(0);
        NumberExpression<Integer> python = post.category
                .name.lower().when("python").then(1).otherwise(0);
        return queryFactory.selectFrom(post)
                .groupBy(postTimePeriodsExp)
                .select(postTimePeriodsExp, java.sum(), python.sum(), post.count())
                .orderBy(postTimePeriodsExp.asc())
                .fetch()
                .stream()
                .map(tuple -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("time_period", tuple.get(postTimePeriodsExp));
                    map.put("java_count", tuple.get(java.sum()));
                    map.put("python_count", tuple.get(python.sum()));
                    map.put("total_count", tuple.get(post.count()));
                    return map;
                }).collect(Collectors.toList());
    }
    
    // 返回
    [
      {
        "time_period": 2015,
        "java_count": 1,
        "python_count": 3,
        "total_count": 6
      },
      {
        "time_period": 2016,
        "java_count": 4,
        "python_count": 2,
        "total_count": 14
      },
      {
        "time_period": 2017,
        "java_count": 3,
        "python_count": 2,
        "total_count": 7
      }
    ]
    

    补充一点

    Spring 参数支持解析 com.querydsl.core.types.Predicate,根据用户请求的参数自动生成 Predicate,这样搜索方法不用自己写啦,比如:

    @GetMapping("posts")
    public Object posts(@QuerydslPredicate(root = Post.class) Predicate predicate) {
        return postRepository.findAll(predicate);
    }
    // 或者顺便加上分页
    @GetMapping("posts")
    public Object posts(@QuerydslPredicate(root = Post.class) Predicate predicate, Pageable pageable) {
        return postRepository.findAll(predicate, pageable);
    }
    

    然后请求:

    /posts?title=title01 // 返回文章 title 为 title01 的文章
    
    /posts?id=2 // 返回文章 id 为 2 的文章
    
    /posts?category.name=Python // 返回分类为 Python 的文章(你没看错,可以嵌套,访问关系表中父类属性)
    
    /posts?user.id=2&category.name=Java // 返回用户 ID 为 2 且分类为 Java 的文章
    

    注意,这样不会产生 SQL 注入问题的,因为不存在的属性写了是不起效果的,Spring 已经进行了判断。 再补充一点,你还可以修改默认行为,继承 QueryDslPredicateExecutor 接口:

    @Repository
    public interface PostRepository extends JpaRepository<Post, Long>, QueryDslPredicateExecutor<Post>, QuerydslBinderCustomizer<QPost> {
        default void customize(QuerydslBindings bindings, QPost post) {
            bindings.bind(post.title).first(StringExpression::containsIgnoreCase);
        }
    }
    

    这样你再访问 /posts?title=title01 时,返回的是文章标题包含 title01 ,而不是仅仅等于的所有文章啦!

    总结

    本文知识抛砖引玉, Querydsl 的强大之处并没有完全体现出来,而且 Spring Boot 官方也提供了良好的支持,所以,掌握了 Querydsl,真的不愁 Java 写不出来的 Sql,重要的是类型安全(没有强制转换),跨数据库(Querydsl 底层还是 JPA 这样的技术,所有只要不写原生 Sql,基本关系型数据库通用)。对了,源代码在这里 example-jpa,可以直接运行的~~ 大家有什么好的经验也可以在评论里提出来啊。

  • Spring Boot JPA - 基本使用

    什么是 JPA ?

    JPA (The Java Persistence API)是用于访问,持久化和管理 Java 对象/类与关系型数据库之间的数据交互的 Java 规范。JPA 被定义为EJB (Enterprise JavaBeans) 3.0规范的一部分,作为 EJB 2 CMP 实体 Bean 规范的替代。

    注意,JPA 只是一个标准,只定义了一系列接口,而没有具体的实现。很多企业级框架提供了对 JPA 的实现,如 Spring 。因此 Spring 本身与 JPA 无关,只是提供了对 JPA 的支持,因此在 Spring 中你也会看到很多注解都是属于 javax.persistence 包的。

    JPA 允许 POJO(Plain Old Java Objects)轻松地持久化,而不需要类来实现 EJB 2 CM P规范所需的任何接口或方法。 JPA 还允许通过注解或 XML 定义对象的关系映射,定义 Java 类如何映射到关系数据库表。 JPA 还定义了一个运行时 EntityManager API,用于处理对象的查询和管理事务。 同时,JPA 定义了对象级查询语言 JPQL,以允许从数据库中查询对象,实现了对数据库的解耦合,提高了程序的可移植性,而不具体依赖某一底层数据库。

    JPA 是 Java 持久化规范中的一个最新版本。第一个版本是 OMG 持久性服务 Java 绑定,但这个一个失败的产品,甚至没有任何商业产品支持它。接下来的版本是 EJB 1.0 CMP Entity Beans,它已经非常成功地被大型 Java EE 提供程序(BEA,IBM)采用,但是它复杂性太高而且性能比较差。EJB 2.0 CMP 试图通过引入本地接口来减少 Entity Bean 的一些复杂性,但是大多数复杂性仍然存在,而且缺乏可移植性。

    历史总是要向前发展的,种种的这些使得 EJB 3.0 规范将降低复杂性作为主要目标,这导致规范委员会沿着 JPA 的路径前进。 JPA 旨在统一 EJB 2 CMP,JDO,Hibernate,从目前来看,JPA 的确取得了成功。

    目前大多数持久化供应商已经发布了 JPA 的实现,并被行业和用户采用。这些包括 Hibernate(由 JBoss 和 Red Hat 收购),TopLink(由 Oracle 收购)和 Kodo JDO(由 BEA 和 Oracle 收购)。其他支持 JPA 的产品包括 Cocobase(由 Thought Inc. 收购)和 JPOX。

    定义实体类

    依赖

    在 Spring Boot 中引入 Spring Data JPA 很简单,在 pom.xml 中加入依赖即可:

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

    Model

    如果具体的 SQL 语句来创建数据库表,就会和具体的底层数据库产生耦合。在 Springl Boot 中,我们可以定义一系列 Entity,来实现 Java 对象到 数据表的映射。

    加入我们的数据表有一些公用的属性,我们可以定义一个超类,来声明这些公用属性。@MappedSuperclass 注解表明实体是一个超类,保证不会被 Spring 实例化 (保险起见加上abstract):

    @MappedSuperclass
    public abstract class BaseModel implements Serializable {
    	/**
    	 *
    	 */
    	private static final long serialVersionUID = 1782474744437162148L;
    	@Id
    	@GeneratedValue(strategy = GenerationType.IDENTITY)
    	@Column(name = "ID")
    	private Long id;
    
    	public Long getId() {
    		return id;
    	}
    
    	public void setId(Long id) {
    		this.id = id;
    	}
    
    	@Override
    	public boolean equals(Object o) {
    		if (this == o) return true;
    		if (o == null || getClass() != o.getClass()) return false;
    
    		BaseModel baseModel = (BaseModel) o;
    
    		return id != null ? id.equals(baseModel.id) : baseModel.id == null;
    	}
    
    	@Override
    	public int hashCode() {
    		return id != null ? id.hashCode() : 0;
    	}
    }
    

    @Id 表明实体主键为整数类型,@GeneratedValue 表明主键值生成策略,strategy 可选值为 TABLESEQUENCEIDENTITYAUTO 。如果我们想对列加上更多的限制,可以使用 @Column 注解,@Column 可以制定列的属性有:name,列名;unique ,是否为一;nullable ,是否为空;length ,长度;columnDefinition,列定义,如 TEXT 类型;等等。

    注意,列名和表名到数据库的映射策略,定义在 org.hibernate.boot.model.naming 包下,你也可以定义自己的映射策略,然后修改 application.yml spring.jpa.hibernate.naming.physical-strategy 的属性即可。

    关系

    以一个简单博客系统来说明吧,包括用户,文章,分类,标签四个表。用户与文章是一对多,一个用户可以有多篇文章;分类与文章也是一对多,文章与标签是多对多: User 表:

    @Entity
    @Table
    public class User extends BaseModel {
        private String name;
        private String email;
        private String address;
    
        @OneToMany(mappedBy = "user")
        private List<Post> posts;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    
        public List<Post> getPosts() {
            return posts;
        }
    
        public void setPosts(List<Post> posts) {
            this.posts = posts;
        }
    }
    

    @OneToMany 声明了 UserPost 的一对多关系,mappedBy 指明了 Post 类中维持该关系的字段名称是 user ,即 Post 表中的 user 字段: Post 表:

    @Entity
    @Table
    public class Post extends BaseModel {
    	private String title;
    
    	@Column(columnDefinition = "TEXT")
    	private String content;
    
    	@ManyToOne
    	@JoinColumn(name = "category_id")
    	private Category category;
    
    	@ManyToMany
    	@JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id"))
    	private Set<Tag> tags;
    
    	private Date createdAt;
    
    	@ManyToOne
    	@JoinColumn(name = "user_id")
    	private User user;
    
    	public String getTitle() {
    		return title;
    	}
    
    	public void setTitle(String title) {
    		this.title = title;
    	}
    
    	public String getContent() {
    		return content;
    	}
    
    	public void setContent(String content) {
    		this.content = content;
    	}
    
    	public Category getCategory() {
    		return category;
    	}
    
    	public void setCategory(Category category) {
    		this.category = category;
    	}
    
    	public User getUser() {
    		return user;
    	}
    
    	public void setUser(User user) {
    		this.user = user;
    	}
    
    	public Date getCreatedAt() {
    		return createdAt;
    	}
    
    	public void setCreatedAt(Date createdAt) {
    		this.createdAt = createdAt;
    	}
    
    	public Set<Tag> getTags() {
    		return tags;
    	}
    
    	public void setTags(Set<Tag> tags) {
    		this.tags = tags;
    	}
    }
    

    Post 类中,因为 UserPost 的一对多关系,所以 user 字段加上了 @JoinColumn(name = "user_id"),表明会在 Post 表中新增加一列 user_id ,因为一对多关系大多数会在“多”的一方加入“一”那一方的主键来做外键(如果不加 @JoinColumn(name = "user_id") ,JPA 会默认新建一个表来维持这种关系)。 Tag 表:

    @Entity
    @Table
    public class Tag extends BaseModel {
    	private String name;
    
    	@ManyToMany
    	@JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "tag_id"), inverseJoinColumns = @JoinColumn(name = "post_id"))
    	private Set<Post> users = new HashSet<>();
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public Set<Post> getUsers() {
    		return users;
    	}
    
    	public void setUsers(Set<Post> users) {
    		this.users = users;
    	}
    }
    

    来说一下多对多,多对多需要一个额外的表来保持关系,所以 PostTag 表都使用了 @JoinTable 注解。其中 name 指的是额外表的名称,当前是 post_tagjoinColumns 指的是当前当前表的主键在额外的表中的名称,如 tag_id 表明 Tag 的 ID 在 post_tag 中名称为 tag_idinverseJoinColumnsjoinColumns 正好相反,PostTag 中他们的属性正好相反。

    Repository

    核心概念

    Spring Data 中最核心的一个接口是 Repository 接口,这是一个空接口,就像 Serializable 接口一样只提供一个标识,供 Spring 动态代理其方法(或者路由到接口的实现上)。实质上,Repository 只是为了获取 Domain Class 和 Domain Class 的 ID 类型等信息。

    但是只提供给开发者一个空接口太不负责了!所以 Spring 内置提供了 CrudRepository (继承自 RepositoryCrud 的意思是增删改查等基本操作),这样我们可以对数据库进行基本的操作。每个方法的意思不用注释,应该能根据它的方法名,参数,返回类型来判断吧:

    public interface CrudRepository<T, ID extends Serializable>
        extends Repository<T, ID> {
    
        <S extends T> S save(S entity); 
    
        T findOne(ID primaryKey);       
    
        Iterable<T> findAll();          
    
        Long count();                   
    
        void delete(T entity);          
    
        boolean exists(ID primaryKey);  
    
        // … more functionality omitted.
    }
    

    当然继承自 CrudRepository 的有一个 PagingAndSortingRepository ,提供了基本的分页操作。

    public interface PagingAndSortingRepository<T, ID extends Serializable>
      extends CrudRepository<T, ID> {
    
      Iterable<T> findAll(Sort sort);
    
      Page<T> findAll(Pageable pageable);
    }
    

    因此在项目中我们可以根据需要继承 RepositoryCrudRepositoryPagingAndSortingRepository 或者 JpaRepository (停供了一些批量操作,如批量删除、增加)。如果你想为项目中所有 Repository 创建一个自定义的基 Repository 来让所有继承自该接口的接口共享方法,可以使用 @NoRepositoryBean 注解,内置的 PagingAndSortingRepository 或者 JpaRepository 也都加了 @NoRepositoryBean @NoRepositoryBean 注解,这表明 Spring 不会在运行时动态生成该接口的实例:

    @NoRepositoryBean
    interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {
    
      T findOne(ID id);
    
      T save(T entity);
    	
      T customSharedMethod(String arg);
    }
    
    interface UserRepository extends MyBaseRepository<User, Long> {
      User findByEmailAddress(EmailAddress emailAddress);
    }
    

    注意到 findOne(…)save(…) 方法 Spring 在 SimpleJpaRepository 类中已经有了实现,所以调用这些方法时会路由到 SimpleJpaRepository 中对用的方法,而自定义的 SimpleJpaRepository 中不存在方法如 T customSharedMethod(String arg) 会由 Spring 动态代理来执行。

    因为 Spring 内置的方法 Repositorys 不可能满足我们所有的需求,所以在 Repository 中自定义自己的方法是必不可少的。

    那么问题来了,我们自定义的方法, Spring 怎么知道如何去执行呢?Spring 有一套自己的查找策略。

    如果你的方法签名比较简单,像 findOne(…) 或者 save(…) ,这些方法的执行会被路由到 SimpleJpaRepository 中对应的方法去执行。而其他的 Spring 则利用自己的机制来解析。

    机制首先找到方法名的 find…By, read…By, query…By, count…By, 或者 get…By 前缀,接着解析剩下的部分。... 的位置亦可以插入一些高级的表达式,如 Distinct 。注意,By 是前缀和真正实体属性的分隔符,By 后面可以添加多个属性,用 And 或者 Or 连接。 例如:

    public interface PersonRepository extends Repository<User, Long> {
    
      // 可以直接根据另一个关联实体进行查找
      List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
    
      // 查找非重复行
      List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
      List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
    
      // 忽略大小写
      List<Person> findByLastnameIgnoreCase(String lastname);
      List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
    
      // 按 Firstname 排序
      List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
      List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
    }
    

    属性表达式

    属性表达式(Property Expressions)只能指向被 Repository 管理的实体的属性,但是也可以指向实体的属性的嵌套属性:假设 Person 有一个 Address 属性,Address 有一个 ZipCode 属性,那么我们可以根据 ZipCode 来查找 Person

    List<Person> findByAddressZipCode(ZipCode zipCode);
    

    这会判断 x.address.zipCode 是否和 zipCode 相等(根据 ZipCode 的主键来判断 )。具体的解析策略是:Spring 检测到整个属性表达式 AddressZipCode ( By 后面) ,然后判断实体类是否有 addressZipCode 这个属性,如果有,则算法停止,使用该属性;否则,算法根据驼峰命名规则,从右边将该属性分为“头”和“尾”两部分,例如 AddressZipCode。如果实体类拥有“头”所指示的属性,然后 Spring 会从此处开始,按照刚才的方法将“尾”拆分,来递归判断是否属于“头”的属性。如果第一次拆分实体类没有“头”所指示的属性,则算法左移一步,拆分为 AddressZipCode 然后继续。

    大所述情况下该算法都会正常工作,但也有例外。比如 PersonaddressZip 属性,但是 AddressZip 没有 code 属性,因此就会在第一次分割后失败。这种情况下,我们可以使用 _ 显示分割:

    List<Person> findByAddress_ZipCode(ZipCode zipCode);
    

    其他

    我们还可以传 PageableSort 参数来实现分页和排序:

    Page<User> findByLastname(String lastname, Pageable pageable);
    
    Slice<User> findByLastname(String lastname, Pageable pageable);
    
    List<User> findByLastname(String lastname, Sort sort);
    
    List<User> findByLastname(String lastname, Pageable pageable);
    

    还可以使用 first 或者 top 限制返回结果的个数:

    User findFirstByOrderByLastnameAsc();
    
    User findTopByOrderByAgeDesc();
    
    Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
    
    Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
    
    List<User> findFirst10ByLastname(String lastname, Sort sort);
    
    List<User> findTop10ByLastname(String lastname, Pageable pageable);
    

    可以返回 Java 8 的 Stream 对象:

    @Query("select u from User u")
    Stream<User> findAllByCustomQueryAndStream();
    
    Stream<User> readAllByFirstnameNotNull();
    
    @Query("select u from User u")
    Stream<User> streamAllPaged(Pageable pageable);
    

    注意,使用 Stream 之后必须手动关闭,或者这样写:

    try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
      stream.forEach(…);
    }
    

    还可以返回异步加载的对象:

    //使用 java.util.concurrent.Future 作为返回对象.
    @Async
    Future<User> findByFirstname(String firstname);               
    
    //使用 Java 8 java.util.concurrent.CompletableFuture 作为返回对象.
    @Async
    CompletableFuture<User> findOneByFirstname(String firstname); 
    
    //使用 org.springframework.util.concurrent.ListenableFuture 作为返回对象.
    @Async
    ListenableFuture<User> findOneByLastname(String lastname);    
    

    附录

    方法名支持的关键字

    | Keyword | Sample | JPQL snippet | |——————-|———————————————————|—————————————————————| | And | findByLastnameAndFirstname | ? where x.lastname = ?1 and x.firstname = ?2 | | Or | findByLastnameOrFirstname | ? where x.lastname = ?1 or x.firstname = ?2 | | Is,Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | ? where x.firstname = ?1 | | Between | findByStartDateBetween | ? where x.startDate between ?1 and ?2 | | LessThan | findByAgeLessThan | ? where x.age < ?1 | | LessThanEqual | findByAgeLessThanEqual | ? where x.age <= ?1 | | GreaterThan | findByAgeGreaterThan | ? where x.age > ?1 | | GreaterThanEqual | findByAgeGreaterThanEqual | ? where x.age >= ?1 | | After | findByStartDateAfter | ? where x.startDate > ?1 | | Before | findByStartDateBefore | ? where x.startDate < ?1 | | IsNull | findByAgeIsNull | ? where x.age is null | | IsNotNull,NotNull | findByAge(Is)NotNull | ? where x.age not null | | Like | findByFirstnameLike | ? where x.firstname like ?1 | | NotLike | findByFirstnameNotLike | ? where x.firstname not like ?1 | | StartingWith | findByFirstnameStartingWith | ? where x.firstname like ?1(parameter bound with appended˙%) | | EndingWith | findByFirstnameEndingWith | ? where x.firstname like ?1(parameter bound with prepended˙%) | | Containing | findByFirstnameContaining | ? where x.firstname like ?1(parameter bound wrapped in˙%) | | OrderBy | findByAgeOrderByLastnameDesc | ? where x.age = ?1 order by x.lastname desc | | Not | findByLastnameNot | ? where x.lastname <> ?1 | | In | findByAgeIn(Collection ages) | ? where x.age in ?1 | | NotIn | findByAgeNotIn(Collection age) | ? where x.age not in ?1 | | TRUE | findByActiveTrue() | ? where x.active = true | | FALSE | findByActiveFalse() | ? where x.active = false | | IgnoreCase | findByFirstnameIgnoreCase | ? where UPPER(x.firstame) = UPPER(?1) |

    方法支持的返回值

    | Return type | Description | |———————-|——————————————————————————————————————————————————————————————————————————————–| | void | 无返回值. | | Primitives | Java primitives. | | Wrapper types | Java wrapper types. | | T | 返回最多一个实体(否咋抛出 IncorrectResultSizeDataAccessException),返回 null 代表未找到. | | Iterator | Java Iterator. | | Collection | Java Collection. | | List | Java List. | | Optional | Java 8 Optional, 返回最多一个实体(否咋抛出 `IncorrectResultSizeDataAccessException`)| | Stream | Java 8 Stream. | | Future | Java Future,需要加 `@Async` 注解,并且Spring 开启异步 | | CompletableFuture | Java CompletableFuture,需要加 `@Async` 注解,并且Spring 开启异步. | | ListenableFuture | `org.springframework.util.concurrent.ListenableFuture`,需要加 `@Async` 注解,并且Spring 开启异步 | | Slice | 一组固定大小的数据,可以判断是否有更多数据,需要 `Pageable` 参数 | | Page | 分页,包括一页的数据,元素总数,总共页数等,需要 `Pageable` 参数 |

    最后

    说了这么多,Spring 提供的方法已经够强大了,基本满足日常需求。但对于更复杂的需求,如投影、关联等高级的查询操作,Spring 就显得心有余而力不足了。下篇介绍Querydsl,看这里Spring Boot JPA - Querydsl,做个引子,说明如果使用 Spring 进行复杂的数据操作。

    参考

    1. Java Persistence/What is JPA?
    2. Spring Data JPA - Reference Documentation
    3. Querydsl Reference Guide
  • 你真的了解如何将 Nginx 配置为Web服务器吗

    阅读之前,建议先阅读初识 Nginx。 之后,我们来了解一下 Nginx 配置。

    抽象来说,将 Nginx 配置为 Web 服务器就是定义处理哪些 URLS ** 和如何处理这些URLS 对应的请求**。具体来说,就是定义一些虚拟服务器(Virtual Servers),控制具有特定 IP 和域名的请求。

    更具体的来说, Nginx 通过定义一系列 locations 来控制对 URIS 的选择。每一个 location 定义了对映射到自己的请求的处理场景:返回一个文件或者代理请求,或者根据不同的错误代码返回不同的错误页面。另外,根据 URI 的不同,请求也可以被重定向到其它 server 或者 location

    设置虚拟服务器

    listen

    Nginx 配置文件至少包含一个 server 命令 ,用来定义虚拟服务器。当请求到来时, Nginx 会首先选择一个虚拟服务器来处理该请求。

    虚拟服务器定义在 http 上下文中的 server 中:

    http {
        server {
            # Server configuration
        }
    }
    

    注意: http 中可以定义多个 server

    server 配置块使用 listen 命令监听本机 IP 和端口号(包括 Unix domain socket and path),支持 IPv4、IPv6,IPv6地址需要用方括号括起来:

    server {
        listen 127.0.0.1:8080;  # IPv4地址,8080端口
    	# listen [2001:3CA1:10F:1A:121B:0:0:10]:80;   # IPv6地址,80端口
    	# listen [::]:80;  # 听本机的所有IPv4与IPv6地址,80端口
        # The rest of server configuration
    }
    

    上述配置,如果不写端口号,默认使用80端口,如果不写 IP ,则监听本机所有 IP。

    server_name

    如果多个 serverlisten IP 和端口号一模一样, Nginx 通过请求头中的 Host server_name 定义的主机名进行比较,来选择合适的虚拟服务器处理请求:

    server {
        listen      80;
        server_name lufficc.com  www.lufficc.com;
        ...
    }
    

    server_name 的参数可以为:

    1. 完整的主机名,如:api.lufficc.com
    2. 含有通配符(含有 *),如:*.lufficc.comapi.*
    3. 正则表达式,以 ~ 开头。

    通配符只能在开头或结尾,而且只能与一个 . 相邻。www.*.example.orgw*.example.org 均无效。 但是,可以使用正则表达式匹配这些名称,例如 ~^www\..+\.example\.org$~^w.*\.example\.org$ 。 而且 * 可以匹配多个部分。 名称 * .example.org 不仅匹配 www.example.org,还匹配www.sub.example.org。 对于正则表达式:Nginx 使用的正则表达式与 Perl 编程语言(PCRE)使用的正则表达式兼容。 要使用正则表达式,且必须以 ~ 开头。

    命名的正则表达式可以捕获变量,然后使用:

    server {
        server_name   ~^(www\.)?(?<domain>.+)$;
    
        location / {
            root   /sites/$domain;
        }
    }
    

    小括号 () 之间匹配的内容,也可以在后面通过 $1 来引用,$2 表示的是前面第二个 () 里的内容。因此上述内容也可写为:

    server {
        server_name   ~^(www\.)?(.+)$;
    
        location / {
            root   /sites/$2;
        }
    }
    

    一个 server_name 示例:

    server {
        listen      80;
        server_name api.lufficc.com  *.lufficc.com;
        ...
    }
    

    同样,如果多个名称匹配 Host 头部, Nginx 采用下列顺序选择:

    1. 完整的主机名,如 api.lufficc.com
    2. 最长的,且以 * 开头的通配名,如:*.lufficc.com
    3. 最长的,且以 * 结尾的通配名,如:api.*
    4. 第一个匹配的正则表达式。(按照配置文件中的顺序)

    即优先级:api.lufficc.com > *.lufficc.com > api.* > 正则。

    如果 Host 头部不匹配任何一个 server_name ,Nginx 将请求路由到默认虚拟服务器。默认虚拟服务器是指:nginx.conf 文件中第一个 server 或者 显式用 default_server 声明:

    server {
        listen      80 default_server;
        ...
    }
    

    配置 location

    URIlocation 参数的匹配

    当选择好 server 之后,Nginx 会根据 URIs 选择合适的 location 来决定代理请求或者返回文件。

    location 指令接受两种类型的参数:

    1. 前缀字符串(路径名称)
    2. 正则表达式

    对于前缀字符串参数, URIs 必须严格的以它开头。例如对于 /some/path/ 参数,可以匹配 /some/path/document.html ,但是不匹配 /my-site/some/path,因为 /my-site/some/path 不以 /some/path/ 开头。

    location /some/path/ {
        ...
    }
    

    对于正则表达式,以 ~ 开头表示大小写敏感,以 ~* 开头表示大小写不敏感。注意路径中的 . 要写成 \. 。例如一个匹配以 ` .html 或者 .htm 结尾的 URIlocation`:

    location ~ \.html? {
        ...
    }
    

    正则表达式的优先级大于前缀字符串。如果找到匹配的前缀字符串,仍继续搜索正则表达式,但如果前缀字符串以 ^~ 开头,则不再检查正则表达式。

    具体的搜索匹配流程如下:

    1. URI 与所有的前缀字符串进行比较。
    2. = 修饰符表明 URI 必须与前缀字符串相等(不是开始,而是相等),如果找到,则搜索停止。
    3. 如果找到的最长前缀匹配字符串以 ^~ 开头,则不再搜索正则表达式是否匹配。
    4. 存储匹配的最长前缀字符串。
    5. 测试对比 URI 与正则表达式。
    6. 找到第一个匹配的正则表达式后停止。
    7. 如果没有正则表达式匹配,使用 4 存储的前缀字符串对应的 location

    = 修饰符拥有最高的优先级。如网站首页访问频繁,我们可以专门定义一个 location 来减少搜索匹配次数(因为搜索到 = 修饰的匹配的 location 将停止搜索),提高速度:

    location = / {
        ...
    }
    

    静态文件和代理

    location 也定义了如何处理匹配的请求:返回静态文件 或者 交给代理服务器处理。下面的例子中,第一个 location 返回 /data 目录中的静态文件,第二个 location 则将请求传递给 https://lufficc.com 域名的服务器处理:

    server {
        location /images/ {
            root /data;
        }
    
        location / {
            proxy_pass https://lufficc.com;
        }
    }
    

    root 指令定义了静态文件的根目录,并且和 URI 拼接形成最终的本地文件路径。如请求 /images/example.png,则拼接后返回本地服务器文件 ` /data/images/example.png` 。

    proxy_pass 指令将请求传递到 URL 指向的代理服务器。让后将来自代理服务器的响应转发给客户端。 在上面的示例中,所有不以 /images / 开头的 URI 的请求都将传递给代理服务器处理。

    比如我把 proxy_pass 设置为 https://www.baidu.com/,那么访问 http://search.lufficc.com/ 将得到百度首页一样的响应(页面)(感兴趣的童鞋可以自己试一试搜索功能,和百度没差别呢):

    server{
          listen 80;
          server_name search.lufficc.com;
          location / {
                  proxy_pass https://www.baidu.com;
          }
    }
    

    使用变量(Variables)

    你可以使用变量来使 Nginx 在不同的请求下采用不同的处理方式。变量是在运行时计算的,用作指令的参数。 变量由 $ 开头的符号表示。 变量基于 Nginx 的状态定义信息,例如当前处理的请求的属性。

    有很多预定义变量,例如核心的 HTTP 变量,你也可以使用 setmapgeo 指令定义自定义变量。 大多数变量在运行时计算,并包含与特定请求相关的信息。 例如,$remote_addr 包含客户端 IP 地址,$uri 保存当前URI值。

    一些常用的变量如下:

    变量名称 作用
    $uri 请求中的当前URI(不带请求参数),它可以通过内部重定向,或者使用index指令进行修改,$uri不包含主机名,如 /foo/bar.html
    $arg_name 请求中的的参数名,即“?”后面的arg_name=arg_value形式的arg_name
    $hostname 主机名
    $args 请求中的参数值
    $query_string $args
    $request 代表客户端的请求地址
    $request_uri 这个变量等于包含一些客户端请求参数的原始URI,它无法修改,不包含主机名,如:/cnphp/test.php?arg=freemouse

    一个简单的应用就是从 http 重定向到 https 时带上路径信息:

    server{
           ...
           return      301 https://lufficc.com$request_uri;
           ...
    }
    

    返回特定状态码

    如果你的网站上的一些资源永久移除了,最快最简洁的方法就是使用 return 指令直接返回:

    location /wrong/url {
        return 404;
    }
    

    return 的第一个参数是响应代码。可选的第二个参数可以是重定向(对应于代码301,302,303和307)的 URL 或在响应正文中返回的文本。 例如:

    location /permanently/moved/url {
        return 301 http://www.example.com/moved/here;
    }
    

    return 指令可以包含在 locationserver 上下文中:

    server{
          location / {
                  return 404;
          }
    }
    

    或者:

    server{
          ...
          return 404;
          location / {
              ...            
          }
    }
    

    错误处理

    error_page 命令可以配置特定错误码的错误页面,或者重定向到其他的页面。下面的示例将在 404 错误发生时返回 /404.html 页面。

    error_page 404 /404.html;
    

    error_page 命令定义了如何处理错误,因此不会直接返回,而 return 确实会立即返回。当代理服务器或者 Nginx 处理时产生相应的错误的代码,均会返回相应的错误页面。

    在下面的示例中,当 Nginx 找不到页面时,它将使用代码301替换代码404,并将客户端重定向到 http://example.com/new/path.html 。 此配置很有用,比如当客户端仍尝试用旧的 URI 访问页面时,301代码通知浏览器页面已永久移除,并且需要自动替换为返回的新地址。

    location /old/path.html {
        error_page 404 =301 http:/example.com/new/path.html;
    }
    

    重写 URIs

    rewrite 指令可以多次修改请求的 URIrewrite 的第一个参数是 URI需要匹配的正则表达式,第二个参数是将要替换的 URI。第三个参数可选,指示是否继续可以重写或者返回重定向代码(301或302)。例如:

    location /users/ {
        rewrite ^/users/(.*)$ /show?user=$1 break;
    }
    

    您可以在 server location 上下文中包括多个 rewrite 指令。 Nginx 按照它们发生的顺序一个一个地执行指令。 当选择 server 时,server 中的 rewrite 指令将执行一次。

    Nginx 处理一组 rewrite 指令之后,它根据新的 URI 选择 location 。 如果所选 location 仍旧包含 rewrite 指令,它们将依次执行。 如果 URI 匹配所有,则在处理完所有定义的 rewrite 指令后,搜索新的 location

    以下示例将 rewrite 指令与 return 指令结合使用:

    server {
        ...
        rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 last;
        rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra  last;
        return  403;
        ...
    }
    

    诸如 /download/some/media/file 的 URI 被改为 /download/some/mp3/file.mp3 。 由于 last 标志,后续指令(第二个 rewrite 指令和 return 指令)被跳过,但 Nginx 继续以更改后的 URI 处理请求。 类似地,诸如 /download/some/audio/file 的 URI 被替换为 /download/some/mp3/file.ra。 如果 URI 不匹配 rewrite 指令,Nginx 将403 错误代码返回给客户端。

    lastbreak的区别是:

    • last : 在当前 serverlocation 上下文中停止执行 rewrite 指令,但是 Nginx 继续搜索与重写的URI匹配的 location,并应用新 location 中的任何 rewrite 指令(这意味着 URI 可能再次改变)。
    • break :停止当前上下文中 rewrite 指令的处理,并取消搜索与新 URI 匹配的 location。 不会执行新 location中的 rewrite 指令。

    附录

    常用正则

    • . : 匹配除换行符以外的任意字符
    • ? : 重复0次或1次
    • + : 重复1次或更多次
    • * : 重复0次或更多次
    • \d :匹配数字
    • ^ : 匹配字符串的开始
    • $ : 匹配字符串的结束
    • {n} : 重复n次
    • {n,} : 重复n次或更多次
    • [c] : 匹配单个字符c
    • [a-z] : 匹配a-z小写字母的任意一个

    全局变量

    • $args : #这个变量等于请求行中的参数,同$query_string
    • $content_length : 请求头中的Content-length字段。
    • $content_type : 请求头中的Content-Type字段。
    • $document_root : 当前请求在root指令中指定的值。
    • $host : 请求主机头字段,否则为服务器名称。
    • $http_user_agent : 客户端agent信息
    • $http_cookie : 客户端cookie信息
    • $limit_rate : 这个变量可以限制连接速率。
    • $request_method : 客户端请求的动作,通常为GET或POST。
    • $remote_addr : 客户端的IP地址。
    • $remote_port : 客户端的端口。
    • $remote_user : 已经经过Auth Basic Module验证的用户名。
    • $request_filename : 当前请求的文件路径,由root或alias指令与URI请求生成。
    • $scheme : HTTP方法(如http,https)。
    • $server_protocol : 请求使用的协议,通常是HTTP/1.0或HTTP/1.1。
    • $server_addr : 服务器地址,在完成一次系统调用后可以确定这个值。
    • $server_name : 服务器名称。
    • $server_port : 请求到达服务器的端口号。
    • $request_uri : 包含请求参数的原始URI,不包含主机名,如:/foo/bar.php?arg=baz
    • $uri : 不带请求参数的当前URI,$uri不包含主机名,如/foo/bar.html
    • $document_uri : 与$uri相同。

    例如请求:http://localhost:88/test1/test2/test.php

    $host localhost
    $server_port 88
    $request_uri /test1/test2/test.php
    $document_uri /test1/test2/test.php
    $document_root /var/www/html
    $request_filename /var/www/html/test1/test2/test.php

    参考

    1. https://www.nginx.com/resources/admin-guide/nginx-web-server/
    2. http://seanlook.com/2015/05/17/nginx-location-rewrite/
  • 初识 Nginx

    Nginx 是一个免费的,开源的,高性能的HTTP服务器和反向代理,以及IMAP / POP3代理服务器。 Nginx 以其高性能,稳定性,丰富的功能,简单的配置和低资源消耗而闻名。很多高知名度的网站都使用 Nginx,如:Netflix,GitHub,SoundCloud,MaxCDN 等。

    Nginx 有一个主线程( master process)和几个工作线程(worker process)。主线程的目的是加载验证配置文件、维护工作线程。

    工作线程处理实际的请求,Nginx 采用基于事件的模型和依赖操作系统的机制在工作线程之间高效地分发请求。工作线程的数量可配置,也可自动调整为服务器CPU的数量。

    Nginx 及其模块的工作方式由配置文件确定。 默认情况下,配置文件名为 nginx.conf,放在 /usr/local/nginx/conf /etc/nginx 或者 /usr/local/etc/nginx 文件夹中。

    基本命令

    Nginx 启动之后,可以使用以下命令控制:

    nginx -s <signal>
    

    其中-s意思是向主进程发送信号,signal可以为以下四个中的一个:

    1. stop — 快速关闭
    2. quit — 优雅关闭
    3. reload — 重新加载配置文件
    4. reopen — 重新打开日志文件

    当运行nginx -s quit时,Nginx 会等待工作进程处理完成当前请求,然后将其关闭。当你修改配置文件后,并不会立即生效,而是等待重启或者收到nginx -s reload信号。

    当 Nginx 收到 nginx -s reload 信号后,首先检查配置文件的语法。语法正确后,主线程会开启新的工作线程并向旧的工作线程发送关闭信号,如果语法不正确,则主线程回滚变化并继续使用旧的配置。当工作进程收到主进程的关闭信号后,会在处理完当前请求之后退出。

    配置文件

    Nginx 配置的核心是定义要处理的 URL 以及如何响应这些 URL 请求,即定义一系列的虚拟服务器(Virtual Servers)控制对来自特定域名或者 IP 的请求的处理。

    每一个虚拟服务器定义一系列的 location 控制处理特定的 URI 集合。每一个location定义了对映射到自己的请求的处理场景,可以返回一个文件或者代理此请求。

    Nginx 由不同的模块组成,这些模块由配置文件中指定的指令控制。 指令分为简单指令块指令

    一个简单指令包含指令名称指令参数,以空格分隔,以分号(;)结尾。 块指令与简单指令类似,但是由大括号({})包围。 如果块指令大括号中包含其他指令,则称该指令为上下文(如: events, http, serverlocation)。

    配置文件中的放在上下文之外的指令默认放在主配置文件中(类似继承主配置文件)。 eventshttp 放置在主配置文件中,server 放置在http块指令中,location放置在server块指令中。

    配置文件的注释以 # 开始。

    静态内容

    Web 服务器一个重要的功能是服务静态文件(图像或静态HTML页面)。例如,Nginx 可以很方便的让服务器从/data/www 获取 html 文件,从/data/images获取图片来返回给客户端,这只需要在http块指令中的server块指令中设置两个location块指令。

    首先,创建 /data/www 目录,并放入 index.html,创建 /data/images 目录并在其中放置一些图片。

    接下来,打开配置文件。 创建一个 server 块:

    http {
        server {
        }
    }
    

    通常,配置文件可以包括多个 server 块,它们以端口服务器名称来区分。当 Nginx 决定某一个 server 处理请求后,它将请求头中的 URIserver 块中的 location 块进行对比。 加入 location 块指令到 server 中:

    将以下位置块添加到服务器块:

    location / {
        root /data/www;
    }
    

    上面的 location 块指定 / 前缀与请求中的 URI 对比。对于匹配的请求,URI 将被添加到 root 指令中指定的路径,即 /data/www,以此形成本地文件系统的路径,如访问http://localhost/bog/welcome.html,对应服务器文件路径为/data/www/bog/welcome.html。 如果 URI 匹配多个 location 块,Nginx 采用最长前缀匹配原则(类似计算机网络里面的IP匹配), 上面的 location 块前缀长度为 1,因此只有当所有其他 location 块匹配时,才使用该块。

    接下来,添加第二个位置块:

    location /images/ {
        root /data;
    }
    

    它将匹配以/images// 也匹配这样的请求,但具有较短的前缀)开始的请求。

    server 块的最终配置如下:

    server {
        location / {
            root /data/www;
        }
    
        location /images/ {
            root /data;
        }
    }
    

    到目前为止,这已经是一个可以正常运行的服务器,它监听端口80,并且可以在本地计算机上访问 http://localhost/。 对于 /images/ 开头的请求,服务器将从 /data/images 目录发送文件。 如,对于 http://localhost/images/example.png 请求,nginx 将响应 /data/images/example.png文件。 如果不存在,nginx 将返回404。URI 不以 /images/ 开头的请求将映射到 /data/www 目录。 例如,对于 http://localhost/some/example.html 请求,nginx 将响应 /data/www/some/example.html 文件。

    代理服务器

    Nginx 的一个常见应用是将其设置为代理服务器(Proxy Server),即接受客户端的请求并将其转发给代理服务器,再接受代理服务器发来的响应,将它们发送到客户端。

    比如我们可以用一个 Nginx 实例实现对图片文件的请求使用本地文件系统,而其他请求转发到代理服务器。

    首先,向 Nginx 的配置文件中添加一个 server 块来定义代理服务器:

    server {
        listen 8080;
        root /data/up1;
    
        location / {
        }
    }
    

    此服务器侦听端口8080,并将所有请求映射到本地文件系统上的 /data/up1 目录。 创建此目录并将 index.html 放入其中。 注意,root 指令放在 server 上下文中,这样 当 location 块中不含 root 指令时将使用所属 serverroot 指令。

    接下来,使用上一节中的服务器配置,并将其修改为代理服务器配置。 在第一个位置块中,加上proxy_pass指令:

    server {
        location / {
    		   # proxy_pass指令的参数为:协议+主机名+端口号
            proxy_pass http://localhost:8080;
        }
    
        location /images/ {
            root /data;
        }
    }
    

    修改第二个 匹配 /images/ 前缀的 location 块,使其与请求图像文件的扩展名相匹配:

    location ~ \.(gif|jpg|png)$ {
        root /data/images;
    }
    

    该参数是一个正则表达式,匹配以.gif,.jpg或.png结尾的所有URI。 正则表达式应该以 ~ 开头。 相应的请求将映射到 /data/images 目录。

    当 Nginx 选择一个 location 块来处理请求时,它首先检查指定 location 块的前缀,记住具有最长前缀的 location 块,然后检查正则表达式。 如果与正则表达式匹配, Nginx 选择此 location 块,否则,选择先前记住的 location 块。

    代理服务器的最终配置如下:

    server {
        location / {
            proxy_pass http://localhost:8080/;
        }
    
        location ~ \.(gif|jpg|png)$ {
            root /data/images;
        }
    }
    

    此服务器将过滤以.gif,.jpg或.png结尾的请求,并将它们映射到 /data/images 目录(通过向 root 指令的参数添加请求的URI),并将所有其他请求发送给上面配置的代理服务器。

    这样,图片和其他请求就可以使用不同的服务器来处理。

    FastCGI代理

    Nginx 可用于将请求路由到 FastCGI 服务器。快速通用网关接口(Fast Common Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。因此 Nginx 可以将请求路由到 FastCGI 运行的应用程序,如 PHP 程序。

    使用 FastCGI 服务器的最基本的 Nginx 配置包括使用 fastcgi_pass 指令而不是 proxy_pass 指令,以及使用 fastcgi_param 指令来设置传递给 FastCGI 服务器的参数。 假设FastCGI服务器可在 localhost:9000 上访问。 以上一节中的代理服务器配置为基础,使用fastcgi_pass指令替换proxy_pass指令,并将参数更改为 localhost:9000 。 在 PHP 中, SCRIPT_FILENAME 参数用于确定脚本名称,而 QUERY_STRING 参数用于传递请求参数。 生成的配置将是:

    server {
        location / {
            fastcgi_pass  localhost:9000;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param QUERY_STRING    $query_string;
        }
    
        location ~ \.(gif|jpg|png)$ {
            root /data/images;
        }
    }
    

    这将设置一个服务器,将路由除静态图像以外的所有请求到运行在 localhost:9000 的 FastCGI 服务器。

    文章来自:http://nginx.org/en/docs/beginners_guide.html

  • 多动态图详细讲解二叉搜索树

    在计算机科学中,二叉搜索树(Binary Search Tree)(有时称为有序或排序的二叉树)是一种能存储特定数据类型的容器。二叉搜索树允许快速查找、添加或者删除某一个节点,并且它是动态的集合。 二叉搜索树按照关键字顺序地保存节点,因此查找和其他操作可以使用二叉搜索原理:当在树(或者寻找插入新节点的地方)中查找节点时,它从根节点遍历到叶节点,与每个节点的关键字进行比较,然后基于比较结果,决定继续在左子树或者右子树中进行搜索。平均而言,每次比较将跳过树的大约一半的元素,这使得每次查找,插入或删除一个节点所花费的时间与树的节点个数的对数成(树的高度)正比,比线性表的性能要好很多。

    定义

    二叉搜索树是以一棵二叉树来组织,每个节点就是一个对象,包括key、卫星数据,除此之外还包括一些为了维持树结构所需要的信息:leftrightparent,分别指向左孩子、右孩子、父节点。其中如果孩子节点或者父节点不存在时,用NIL表示。根节点是树中唯一一个父节点为NIL的节点。

    二叉搜索树具有以下性质:

    1. 如果节点的左子树不空,则左子树上所有结点的值均小于等于它的根结点的值;
    2. 如果节点的右子树不空,则右子树上所有结点的值均大于等于它的根结点的值;
    3. 任意节点的左、右子树也分别为二叉查找树;

    二叉树{.figure}

    比如在上图中根节点的关键字为6,左子树有关键字2、4和5,均不大于6;右子树有关键字7和8,均不小于6。这个性质对树中的每个节点都成立,也就是说,二叉搜索树的定义是递归的。

    在讨论二叉搜索树的操作之前,先看看二叉搜索树的遍历。二叉搜索树可以使用先序遍历(preorder tree walk)、中序遍历(inorder tree walk)和后序遍历(postorder tree walk)。这样命名的依据是根据输出关键字相对于左右子树的位置。以中序遍历为例,伪代码如下:

    INORDER-TREE-WALK(x)
    	if x != nil  // 如果节点不为空
    		INORDER-TREE-WALK(x.left)  // 首先递归地遍历左孩子,直到左孩子为空
    		print x.key     // 输出当前节点(显然第一次运行到这里时,它是最小值,因为它是整棵树的最左节点)  
    		INORDER-TREE-WALK(x.right)  // 递归地遍历右孩子
    

    对于上图中的二叉搜索树,动态过程如下,这样输出结果为:2,4,5,6,7,8,即按照从小到大的顺序排列。因为输出时一直遍历左孩子,知道遇到第一个左孩子为空的节点,将它输出,然后出栈返回继续输出。

    file

    查询

    二叉搜索树还应该可以完成MINIMUMMAXIMUMSUCCESSORPREDECESSOR操作,即求最小值,最大值,后继和前驱,并且这些操作都可以在o(lgn)的时间内完成。

    查找指定关键字

    TREE-SEARCH操作在二叉树中查找一个具有指定的关键字的节点,输入树的根节点指针和关键字k,如果存在,返回节点指针,否则,返回nil

    TREE-SEARCH(x, k)
    	if x == nil or k == x.key  //如不存在或者找到,直接返回
    		return x
    	if k < x.key                    //如果小于当前节点,根据性质,在左子树中搜索
    		return TREE-SEARCH(x.left, k)
    	else                          //如果大于等于当前节点,根据性质,在右子树中搜索
    		return TREE-SEARCH(x.right, k)
    

    比如查找关键字为5的节点,首先从根节点6开始,与5进行比较,因为5小于6,因此在节点6的左子树继续搜索。到达节点4时因为5大于4,所以在4节点的右子树搜索,这样就顺利找到了节点5,此时函数将返回指向节点5的指针。如果找不到目标节点,TREE-SEARCH函数将返回nil。整个搜索过程如下:

    搜索过程{.figure}

    最小/最大关键字

    通过从树根开始,沿着left孩子向下搜索,直到遇到nil,那么根据二叉搜索树的性质,如果节点x没有左子树,而x的右子树的关键字肯定都大于x.key,因此此时当前节点一定是整个树中的最小值。

    TREE-MINIMUM(x)
    	while x.left != nil  // 沿着左子树一直深入搜索下去,直到遇到左子树为空的节点,此时当前节点为最小值
    		x = x.left
    	return x
    

    同理,最大关键字的伪代码如下:

    TREE-MAXIMUM(x)
    	while x.right != nil  // 沿着右子树一直深入搜索下去,直到遇到右子树为空的节点,此时当前节点为最大值
    		x = x.right
    	return x
    

    求取最大、最小关键字的时间复杂度仅为o(lgn),即与树的高度成正比,因为查找过程自上而下形成一条线,线的最大长度为数的高度,如求取最小值的过程:

    求取最小值{.figure}

    前驱/后继

    给定二叉搜索树的一个节点,有事需要按照中序遍历的次序查找它的后继,如果所有的关键字互不相同,则一个节点x的后继一定是大于x.key的最小关键字。

    TREE-SUCCESSOR(x)
    	if x.right != nil   //case 1:如果右子树不为空,则后继一定是右子树的最小值,即大于x的最小值(右子树的值都大于x节点)
    		return TREE-MINIMUM(x.right)
    	y = x.p    // case 2:右子树为空时
    	while y != nil and x == y.right
    		x = y   // 变量x代表节点原始x的祖先,如果找到x,它是父节点的左孩子,则循环终止
    		y = y.p   // y 代表节点x的父节点,如果x是y的左孩子,循环终止,并且返回y
    	return y
    

    1.对于第一种情况比较简单,如果x右子树不为空,那它的后继就是右子树的最左节点,对应伪代码case 1,例如下图寻找68的后继,即寻找68的右子树的最小节点72,同时它也是右子树的最左节点。

    寻找68的后继{.figure}

    2.第二种情况是x的右子树为空,注意x的后继始终是大于x的最小值(或者不存在),所以当x的右子树不存在时大于x的最小值在哪儿呢?我们只需要简单的从x开始沿树而上,找到第一个这样一个节点:它的父节点为空(即根节点)或者它的左孩子是x节点的祖先节点(不一定是直接祖先)。例如下图中为了寻找17的后继,沿着树上升,首先以此遇到了节点1311,它们均不符合条件,因为它们不是父节点的左孩子。当遇到节点10时,此时x指向节点10y指向节点19,并且节点10是节点19的左孩子,符合条件,所以返回节点y,它是节点x的后继。

    寻找17的后继{.figure}

    再举一个例子,下图为了找15的后继,仍然沿着树上升,直到遇到节点10(此时伪代码中的变量x指向节点10):它是15的祖先,而且是左孩子。所以此时返回节点10的父节点19,即节点15的后继。

    寻找15的后继{.figure}

    一个二叉搜索树中除了最大节点外,都有后继。对于前驱节点,和后继节点原理一样,这里不再赘述。

    插入

    插入操作会引起二叉搜索树集合的动态变化,因此需要一定的修改来维持二叉搜索树。由于二叉搜索树的性质,即左孩子小于等于父节点,右孩子大于等于父节点,因此插入操作相对简单。

    将一个节点插入到二叉搜索树中,需要调用TREE-INSERT,该过程以节点z作为输入,其中z.left = nil, z.right = nil, z.key = 将要插入数据的关键字

    TREE-INSERT(T, z)
    	y = nil
    	x = T.root
    	while x != nil //循环结束后,x一定为空,此时x即为节点z要插入的地方
    		y = x    //在这里给y赋值,保证循环结束后y始终是x的父节点
    		if z.key < x.key
    			x = x.left
    		else
    			x = x.right
    		z.p = y  //  y始终是x的父节点,为了插入z,需要让z的父节点指向x的父节点,即指向y
    		if y == nil  //  如果y为空,说明插入时是一棵空的树,需要将树根指向z
    			T.root = z
    		elseif z.key < y.key   //  判断节点z是y的左孩子还是右孩子
    			y.left = z
    		else
    			y.right = z
    

    上述伪代码从树根开始,指针x记录了一条向下的简单路径,通过while循环比较z.keyx.key的大小,使指针x和指针y向下移动,循环结束时则找到一个空的x并作为一个槽,将节点z放到这里(插入),同时保持节点y为节点x的父节点,这样可以很方便的决定插入之后将z作为它的左孩子还是右孩子。举一个例子:

    file

    上图为了在树中插入节点46,首先x指向根节点,节点46与根节点68x节点)比较,小于68,因此指针x指向根节点(x节点)的左孩子62,然后一直下移。注意当x指向45的时候,节点46大于45,因此将x指向节点45的右孩子,此时xnil了,循环结束,也就找到了节点46的位置:节点45的右孩子。然后进行一些操作将节点46插入到树中即可。

    删除

    从二叉搜索树中删除一个节点z稍微有点棘手,但总的来说可以分为三种情况:

    1. 如果z没有孩子节点,那么简单的将它删除,并修改它的父节点,用nil作为孩子节点代替z即可。
    2. 如果z只有一个孩子,那么将这个孩子提升到z的位置,并修改它的父节点,用z的孩子代替z即可。
    3. 如果z有两个孩子,那么用z的后继y(此时z的后继y一定在z的右子树中,因为z的右孩子不为空)来占据z的位置,此时z的原来的右子树部分称为y的新的右子树,并且z的左子树称为y的新的左子树。这种情况稍微麻烦,因为还与y是否为z的右孩子相关。

    第一种情况:节点z没有孩子

    这种情况比较简单,我们直接删除节点z即可,并不会影响到二叉搜索树的性质:

    file

    用动图来表示就是:

    file

    第二种情况:节点z只有一个孩子

    这种情况也比较简单,直接用节点z的孩子代替节点z即可。其实第一种情况和第二种情况可以归为一个:节点z的孩子个数小于2个,直接用节点z的孩子代替节点z即可,只是节点z没有孩子时是用的nil代替节点z,这里为了更加清楚地说明分了三种情况。

    file

    例如如下图,当节点42只有左孩子时,直接将42的父节点6的右孩子指向节点29,将节点29的父节点设置为节点6即可:

    file

    或者只有右孩子时也是如此,直接将94的左孩子指向7878的父节点指向94即可: file

    第三种情况:节点z有两个孩子

    这种情况稍微复杂一点,因为此时我们需要找到节点z的后继y,而后继节点y又分为y是节点z的直接右孩子或者不是。

    1. z的后继yz的右孩子 此时可以直接用后继y代替z,而且y的左孩子此时一定为空(因为后继的左孩子一定为空),再用z的左孩子代替y的原来为空的左孩子即可。 file 用动图表示删除节点67就是: file

    2. z的后继y不是 z的右孩子 在这种情况下我们先用y的右孩子x代替y,然后再用y代替z:

    file

    file

    用动图表示删除节点50就是用74代替73,即将73的父节点82的右孩子指向74,74的父节点设置为82,然后再用73代替50,即将50左孩子31设置为73的左孩子,50的右孩子82设置为73的右孩子: file

    为了实现删除过程的伪代码,我们需要定义一个子过程TRANSPLANT,它是用为了用以v为根的子树替换以u为根的子树,让u的双亲节点变为v的双亲节点,即让v称为u的双亲节点的孩子:

    TRANSPLANT(T, u, v)
    	if u.p == nil   // 当u位树的根节点时,直接将树的根节点指向v
    		T.root = v
    	elseif u == u.p.left   // 如果u是左孩子,则u的父节点的左孩子指向v
    		u.p.left = v
    	else    // 如果u是右孩子,则u的父节点的右孩子指向v
    		u.p.right = v
    	if v != nil   
    		v.p = u.p    // 将v的父节点设为u的父节点
    

    然后实现具体的删除过程:

    TREE-DELETE(T, z)
    	if z.left == nil  // 如果左孩子为空,则直接用右孩子代替z即可,而不管右孩子是否为空(右孩子为空时对应情况一否则对应情况二)
    		TRANSPLANT(T, z, z.right)
    	elseif z.right == nil  // 右孩子为空,直接用做孩子代替z
    		TRANSPLANT(T, z, z.left)
    	else y = TREE-MINIMUM(z.right)  // 左右孩子均不为空,找到z的后继y,即z的右子树的最小值,对应第三种情况
    		if y.p != z   // 如果z的后继y不是z的右孩子,对应第三种情况的2
    			TRANSPLANT(T, y, y.right)  // 用y的右孩子代替y
    			y.right = z.right    // 将y的右孩子指向z的右孩子
    			y.right.p = y     // 将y的右孩子(原来的z的右孩子)的父节点设为y
    		TRANSPLANT(T, z, y)  // 用y代替z
    		y.left = z.left
    		y.left.p = p
    

    因此总的来说,删除操作可以分为两大类:

    1. z的孩子总数小于2时,直接用z的孩子代替z即完成了对z的删除。
    2. z有两个孩子时: 2.1. z的后继yz的右孩子:直接用y代替z即可(别忘了将z的左孩子的父节点设置为y)。 2.2. z的后继y不是z的右孩子:先用y的右孩子x代替y,再用y代替z

    总结

    因为二叉搜索树的性质,即可以在每个比较之后将数据规模变为原来的一半,因此平均情况下每一个操作都可以在o(lgn)的时间内完成,即花费时间与树的高度成正比。但在最坏的情况下,二叉搜索树就退化为一个链表,此时的时间复杂度退化到了o(n)。但很多改进版的二叉查找树可以使树高为o(lgn),如SBT,AVL树,红黑树等。

    参考文献

    1. 算法导论
    2. 维基百科
    3. visualgo