Spring Data JPA通过方法名查询实战+源码分析

Stella981
• 阅读 1065

前几天接触到SpringDataJPA访问数据库的项目,看源代码时发现有的Repository上面的部分接口没有实现类,但是系统却可以正常运行,这引起了我的好奇心,决定花点时间研究下,于是便有了此文。

先来看看是哪些接口可以不用实现:

Xxx findByXxxAndXxOrderByXxDesc(String arg1, String arg2, String arg3);

带着这样的疑问,开始今天的探索。

实战篇

1. 创建一个简单的表:

Spring Data JPA通过方法名查询实战+源码分析

2. 创建一个Gradle的SpringBoot项目,并创建一个User实体类:

package com.example.demo.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    private Integer age;

    @Override
    public String toString() {
        return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}';
    }
    
    // Getters and Setters ...
}

3. 创建UserRepo类作为User对象访问数据库的操作类,这里需要注意的是,UserRepo类继承了JpaRepository类:

package com.example.demo.dao;

import com.example.demo.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepo extends JpaRepository<User, Long> {
    // 通过三个列查找User
    User findByIdAndNameAndAge(Long id, String name, Integer age);
}

4. 写个单元测试看下效果:

package com.example.demo;

import com.example.demo.dao.UserRepo;
import com.example.demo.domain.User;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private UserRepo userRepo;

    @Test
    void testFindByIdAndNameAndAge() {
        User user = userRepo.findByIdAndNameAndAge(1L, "George", 20);
        System.out.println("==========>>>>>>>>>>" + user.toString());
    }
}

5. 执行结果:

Spring Data JPA通过方法名查询实战+源码分析

可以看到这个userRepo.findByIdAndNameAndAge方法只存在于接口,并没有任何实现(一句SQL都没写),但还是成功的返回了执行结果。

为什么呢?接下来我们来看源码。

源码篇

再次运行单元测试,这次用debug模式看看:

Spring Data JPA通过方法名查询实战+源码分析

可以看到,Spring为我们注入的userRepo对象是个代理对象,实际上是个SimpleJpaRepository对象,而我们的UserRepo类是这么声明的:

public interface UserRepo extends JpaRepository<User, Long>

它继承于JpaRepository接口,我们可以从这个JpaRepository的实现类或子接口关系中找到这个实际注入的SimpleJpaRepository类:

Spring Data JPA通过方法名查询实战+源码分析

那么为什么会注入这个类呢?先来回答这个问题。

我们在Java Config方式配置Spring Data JPA的@EnableJpaRepositories注解里有这样一句:

//源码1
Class<?> repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class;

配置中默认指定了Repository的工厂类为JpaRepositoryFactoryBean,从类名可以看出这个类是一个工厂类,看下源码:

//源码2
public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
        extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> {

    private @Nullable EntityManager entityManager;
    
    // 省略部分代码 ....

    /*
     * (non-Javadoc)
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    @Override
    public void afterPropertiesSet() {
        Assert.state(entityManager != null, "EntityManager must not be null!");
        super.afterPropertiesSet();
    }

    // 省略部分代码 ...
}

可以看到,这是个这个类覆盖了afterPropertiesSet()方法,跟踪一下发现是在RepositoryFactoryBeanSupport类中的实现:

//源码3
public void afterPropertiesSet() {
    // 看这里。这里初始化了一个repositoryFactory
    this.factory = createRepositoryFactory();
    
    // 省略部分代码 ...

    // 看这里,从repositoryFactory中获取对象
    this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));

    if (!lazyInit) {
        this.repository.get();
    }
}

这里面初始化了一个repositoryFactory,并从这个factory中获取repository,跟一下这个createRepositoryFactory()方法,最后会发现,在JpaRepositoryFactoryBean中有实现:

//源码4
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
    // 看这里。初始化了一个JpaRepositoryFactory
    JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);

    // 省略部分代码 ...

    return jpaRepositoryFactory;
}

再跟一下this.factory.getRepository()方法,在RepositoryFactorySupport类中:

//源码5
public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {

    if (LOG.isDebugEnabled()) {
        LOG.debug("Initializing repository instance for {}…", repositoryInterface.getName());
    }

    Assert.notNull(repositoryInterface, "Repository interface must not be null!");
    Assert.notNull(fragments, "RepositoryFragments must not be null!");

    RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface);
    RepositoryComposition composition = getRepositoryComposition(metadata, fragments);
    RepositoryInformation information = getRepositoryInformation(metadata, composition);

    validate(information, composition);

    // 看这里。通过information创建了target,并将其放入到代理对象的target中
    Object target = getTargetRepository(information);

    // Create proxy
    ProxyFactory result = new ProxyFactory();
    result.setTarget(target);
    result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);

    if (MethodInvocationValidator.supports(repositoryInterface)) {
        result.addAdvice(new MethodInvocationValidator());
    }

    result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);

    postProcessors.forEach(processor -> processor.postProcess(result, information));

    if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) {
        result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
    }

    ProjectionFactory projectionFactory = getProjectionFactory(classLoader, beanFactory);

    // QueryLookupStrategy稍后会介绍
    Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
            evaluationContextProvider);
    // QueryExecutorMethodInterceptor稍后会介绍
    result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,
            namedQueries, queryPostProcessors));

    composition = composition.append(RepositoryFragment.implemented(target));
    result.addAdvice(new ImplementationMethodExecutionInterceptor(composition));

    T repository = (T) result.getProxy(classLoader);

    if (LOG.isDebugEnabled()) {
        LOG.debug("Finished creation of repository instance for {}.", repositoryInterface.getName());
    }

    return repository;
}

注意: 在创建ProxyFactory实例后, 调用getProxy()返回的是Repository接口代理。这也是常说的: Spring Data JPA 的实现原理是动态代理机制

JpaRepositoryFactory.getTargetRepository():

//源码6
protected final JpaRepositoryImplementation<?, ?> getTargetRepository(RepositoryInformation information) {
    // 看这里。
    JpaRepositoryImplementation<?, ?> repository = getTargetRepository(information, entityManager);
    repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata());
    repository.setEscapeCharacter(escapeCharacter);

    return repository;
}

里面调用了getTargetRepository重载方法,跟进去:

//源码7
protected JpaRepositoryImplementation<?, ?> getTargetRepository(RepositoryInformation information,
        EntityManager entityManager) {

    JpaEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType());
    
    // 看这里。这里得到了一个repository
    Object repository = getTargetRepositoryViaReflection(information, entityInformation, entityManager);

    Assert.isInstanceOf(JpaRepositoryImplementation.class, repository);

    return (JpaRepositoryImplementation<?, ?>) repository;
}

//源码8
protected final <R> R getTargetRepositoryViaReflection(RepositoryInformation information,
            Object... constructorArguments) {
    // 看这里。根据information获取到repository的类名,然后通过反射生成实例并返回
    Class<?> baseClass = information.getRepositoryBaseClass();
    return getTargetRepositoryViaReflection(baseClass, constructorArguments);
}

可以看到,最终通过RepositoryInformation对象获取到repository的类名,并通过反射生成实例最终返回。那么这个RepositoryInformation是如何生成的呢?细心的同学可能已经看到了,在【源码5】中,通过getRepositoryInformation方法初始化的。进去看看具体过程:

//源码9
private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,
        RepositoryComposition composition) {

    RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition);

    return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {
        //看这里。baseClass在这里初始化的
        Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));

        return new DefaultRepositoryInformation(metadata, baseClass, composition);
    });
}

debug的时候可以看到,这里的repositoryBaseClass是空的:

Spring Data JPA通过方法名查询实战+源码分析

那么baseClass一定是从getRepositoryBaseClass()方法中被初始化的:

//源码10
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
    return SimpleJpaRepository.class;
}

源码跟到这里,就知道为什么注入的是SimpleJpaRepository了。接下来分析方法名是怎么被转换成SQL的。

QueryLookupStrategy和QueryExecutorMethodInterceptor

在前面的【源码5】中,返回的代理中,加入了方法拦截器QueryExecutorMethodInterceptor,这个拦截器的作用就是拦截查询方法的。它使用一个QueryLookupStrategy来决定从哪里取查询。在【源码5】中创建了一个queryLookupStrategy:

//源码5
Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
        evaluationContextProvider);
result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,
        namedQueries, queryPostProcessors));

接下来看看QueryExecutorMethodInterceptor的构造器:

//源码11
/**
 * Creates a new {@link QueryExecutorMethodInterceptor}. Builds a model of {@link QueryMethod}s to be invoked on
 * execution of repository interface methods.
 */
public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation,
        ProjectionFactory projectionFactory, Optional<QueryLookupStrategy> queryLookupStrategy, NamedQueries namedQueries,
        List<QueryCreationListener<?>> queryPostProcessors) {

    this.namedQueries = namedQueries;
    this.queryPostProcessors = queryPostProcessors;

    this.resultHandler = new QueryExecutionResultHandler(RepositoryFactorySupport.CONVERSION_SERVICE);

    if (!queryLookupStrategy.isPresent() && repositoryInformation.hasQueryMethods()) {

        throw new IllegalStateException("You have defined query method in the repository but "
                + "you don't have any query lookup strategy defined. The "
                + "infrastructure apparently does not support query methods!");
    }

    this.queries = queryLookupStrategy //
            .map(it -> mapMethodsToQuery(repositoryInformation, it, projectionFactory)) //
            .orElse(Collections.emptyMap());
}

可以看到,QueryExecutorMethodInterceptor用一个Map<Method, RepositoryQuery> queries缓存了Repository里面定义的所有方法以便于在后续过程中通过方法名直接找出对应的查询。这些查询来自参数queryLookupStrategy,queryLookupStrategy则来自【源码5】。那么再看看这个getQueryLookupStrategy方法,它返回了一个QueryLookupStrategy的对象,它在RepositoryFactorySupport类中有一个默认的空实现,但真正的实现在JpaRepository中:

//源码12
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
        QueryMethodEvaluationContextProvider evaluationContextProvider) {
    return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, evaluationContextProvider,
            escapeCharacter));
}

继续跟这个JpaQueryLookupStrategy.create()方法:

//源码13
public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
        @Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider, EscapeCharacter escape) {

    Assert.notNull(em, "EntityManager must not be null!");
    Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");

    switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) {
        case CREATE:
            return new CreateQueryLookupStrategy(em, queryMethodFactory, escape);
        case USE_DECLARED_QUERY:
            return new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider);
        case CREATE_IF_NOT_FOUND:
            return new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory,
                    new CreateQueryLookupStrategy(em, queryMethodFactory, escape),
                    new DeclaredQueryLookupStrategy(em, queryMethodFactory, evaluationContextProvider));
        default:
            throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
    }
}

方法的查询策略有三种:

  1. CREATE: 通过解析规范命名的方法名来创建查询, 此时即使有@Query@NameQuery也会忽略。如果方法名不符合规则,启动的时候会报异常
  2. USE_DECLARED_QUERY: 也就是使用注解方式声明式创建, 启动时就会去尝试找声明的查询, 也就是使用@Query定义的语句来执行查询, 没有则找@NameQuery的, 再没有就异常.
  3. CREATE_IF_NOT_FOUND: 先找@Query@NameQuery定义的语句来查询, 如果没有就通过解析方法名来创建查询.

本文主要分析方法名是如何创建查询的,所以这里我们只看1。直接进入CreateQueryLookupStrategy类,它是JpaQueryLookupStrategy的内部类:

//源码14
/**
 * {@link QueryLookupStrategy} to create a query from the method name.
 *
 * @author Oliver Gierke
 * @author Thomas Darimont
 */
private static class CreateQueryLookupStrategy extends AbstractQueryLookupStrategy {

    private final EscapeCharacter escape;

    public CreateQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
            EscapeCharacter escape) {

        super(em, queryMethodFactory);

        this.escape = escape;
    }

    @Override
    protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
        // 看这里。创建了一个PartTreeJpaQuery对象
        return new PartTreeJpaQuery(method, em, escape);
    }
}

从源码的注释里面可以看到,QueryLookupStrategy to create a query from the method name(它是一个通过方法名来创建query的查询策略)。并且resolveQuery方法覆盖了父类AbstractQueryLookupStrategy的方法。先来看看父类:

//源码15
private abstract static class AbstractQueryLookupStrategy implements QueryLookupStrategy {

    // 省略部分代码 ...

    @Override
    public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
            NamedQueries namedQueries) {
        return resolveQuery(queryMethodFactory.build(method, metadata, factory), em, namedQueries);
    }

    // 看这里。resolveQuery方法是abstract的,待子类实现
    protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries);
}

果然,在父类有个resolveQuery方法,并且是abstract的,在子类CreateQueryLookupStrategy中被实现,并创建了一个PartTreeJpaQuery对象。继续跟进这个PartTreeJpaQuery类:

//源码16
PartTreeJpaQuery(JpaQueryMethod method, EntityManager em, EscapeCharacter escape) {

    super(method, em);

    this.em = em;
    this.escape = escape;
    Class<?> domainClass = method.getEntityInformation().getJavaType();
    this.parameters = method.getParameters();

    boolean recreationRequired = parameters.hasDynamicProjection() || parameters.potentiallySortsDynamically();

    try {
        // 看这里。关键的PartTree对象
        this.tree = new PartTree(method.getName(), domainClass);
        validate(tree, parameters, method.toString());
        this.countQuery = new CountQueryPreparer(recreationRequired);
        this.query = tree.isCountProjection() ? countQuery : new QueryPreparer(recreationRequired);
    } catch (Exception o_O) {
        throw new IllegalArgumentException(
                String.format("Failed to create query for method %s! %s", method, o_O.getMessage()), o_O);
    }
}

最核心的部分来了。这个PartTreeJpaQuery持有了一个PartTree对象,该对象会将一个方法名解析成一个PartTree。打开PartTree类看看:

//源码17
/**
 * Class to parse a {@link String} into a tree or {@link OrPart}s consisting of simple {@link Part} instances in turn.
 * Takes a domain class as well to validate that each of the {@link Part}s are referring to a property of the domain
 * class. The {@link PartTree} can then be used to build queries based on its API 
 * instead of parsing the method name for each query execution.
 *
 * @author Oliver Gierke
 * @author Thomas Darimont
 * @author Christoph Strobl
 * @author Mark Paluch
 * @author Shaun Chyxion
 */
public class PartTree implements Streamable<OrPart> {

    /*
     * We look for a pattern of: keyword followed by
     *
     *  an upper-case letter that has a lower-case variant \p{Lu}
     * OR
     *  any other letter NOT in the BASIC_LATIN Uni-code Block \\P{InBASIC_LATIN} (like Chinese, Korean, Japanese, etc.).
     *
     * @see <a href="https://www.regular-expressions.info/unicode.html">https://www.regular-expressions.info/unicode.html</a>
     * @see <a href="https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#ubc">Pattern</a>
     */
    private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))";
    // 看这里。查询方法以这些关键字开头都可以。比如:findById, getByNameAndAge
    private static final String QUERY_PATTERN = "find|read|get|query|search|stream";
    private static final String COUNT_PATTERN = "count";
    private static final String EXISTS_PATTERN = "exists";
    // 删除的关键字为delete或remove。比如:removeById, deleteByIdAndName等
    private static final String DELETE_PATTERN = "delete|remove";
    // 匹配方法名的正则表达式
    private static final Pattern PREFIX_TEMPLATE = Pattern.compile( //
            "^(" + QUERY_PATTERN + "|" + COUNT_PATTERN + "|" + EXISTS_PATTERN + "|" + DELETE_PATTERN + ")((\\p{Lu}.*?))??By");

    /**
     * The subject, for example "findDistinctUserByNameOrderByAge" would have the subject "DistinctUser".
     */
    // 主语对象,就是要查找的东西。
    private final Subject subject;

    /**
     * The predicate, for example "findDistinctUserByNameOrderByAge" would have the predicate "NameOrderByAge".
     */
    // 谓语对象,就是通过什么查找。
    private final Predicate predicate;

    /**
     * Creates a new {@link PartTree} by parsing the given {@link String}.
     *
     * @param source the {@link String} to parse
     * @param domainClass the domain class to check individual parts against to ensure they refer to a property of the
     *          class
     */
    public PartTree(String source, Class<?> domainClass) {

        Assert.notNull(source, "Source must not be null");
        Assert.notNull(domainClass, "Domain class must not be null");

        Matcher matcher = PREFIX_TEMPLATE.matcher(source);

        if (!matcher.find()) {
            this.subject = new Subject(Optional.empty());
            this.predicate = new Predicate(source, domainClass);
        } else {
            this.subject = new Subject(Optional.of(matcher.group(0)));
            this.predicate = new Predicate(source.substring(matcher.group().length()), domainClass);
        }
    }

    // 省略很多代码 ...

}

从注释可以看到,这个类会通过正则表达式Matcher来解析一个方法名(字符串)为一个一个的Part(最后形成一个PartTree),另一个Class类型的参数是用于校验解析到的每个Part都能跟这个类的某个属性对应上,最后这个PartTree对象会由API转换成一个Query。

这个PartTree包含了一个主语(要查询什么)和一个谓语(通过什么查询),我们还可以从源码中看到匹配的各种关键字等。

看到这里,就可以回答了一开始的问题了:为什么以下方法没有任何实现,但是却可以正确执行?

User user = userRepo.findByIdAndNameAndAge(1L, "George", 20);

答案就是,SpringDataJPA帮我们把每个方法名解析成了一个PartTree对象,然后转换成了一个Query,最后生成SQL来执行了。

需要注意的是,只有符合规范的方法名才能被正确的转换成查询,这里附上Spring官方文档,介绍了哪些支持的关键字:

The following table describes the keywords supported for JPA and what a method containing that keyword translates to:

Table 3. Supported keywords inside method names

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, Null

findByAge(Is)Null

… 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<Age> ages)

… where x.age in ?1

NotIn

findByAgeNotIn(Collection<Age> ages)

… 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)

完。

(完整代码:https://github.com/wanxiaolong/JpaDemo)

参考资料:

https://docs.spring.io/spring-data/jpa/docs/2.3.2.RELEASE/reference/html/#reference

https://www.jianshu.com/p/d05ba90d19e7

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这