后端总结

Spring JPA

依赖与配置

build.gradle

compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.flywaydb:flyway-core')
runtime('mysql:mysql-connector-java')

application.yml

spring:
  datasource:
    password: ***
    username: root
    url:  jdbc:mysql://127.0.0.1:3306/{database_name}?characterEncoding=UTF-8
  jpa:
    show-sql: true
    database: mysql

mysql中创建对应的数据库,然后配置flyway:在application.yml同级目录下创建db.migration文件夹,以V{year}{month}{day}{time}__{action}.sql写入创建表的脚本(eg.V201808111253__init_table.sql)。

Entity 实体对象

对应数据库中的表,储存表中的相应内容示例如下:

Product:

@Entity
@Table(name="product")
public class Product {
    @Id
    @GenerateValue(strategy=GenerateType.IDENTITY)
    private Id;
    @Coloum(name="product_name")
    private name;
    ...
}

Controller HTTP消息分发

ProductController

@RestController
@RequestMapping
public class ProductController {
    @Autowired
    private ProductService productService;

    @GetMapping("/{id}")
    public ResponseEntity get(@PathVariable Long id) {
        return ResponeEntity.ok(productService.get(id));
    }
    ...
}

Service 业务逻辑处理

productService

@Service
public class ProductService {
    @AutoWired
    private ProductRepository productRepository;

    public Product get(Long id) {
        return productRepository
            .findById(id)
            .orElseThrow(ProductNotFoundException::new)
    }
    ...
}

Repository 与数据库交互

ProductRepository(自定义数据库)

public interface CustomProductRepository {
    List<Product> searchProduct(Double minPrice, Double maxPrice, String brand, String category, Integer pageNum, Integer pageSize, String order);
}
@Repository
public class CustomProductRepositoryImpl implements CustomProductRepository {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Product> searchProduct(Double minPrice, Double maxPrice, String brand, String category, Integer pageNum, Integer pageSize, String order) {
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Product> query = builder.createQuery(Product.class);
        Root<Product> root = query.from(Product.class);

        Predicate predicate = builder.conjunction();

        if (minPrice != null) {
            predicate = builder.and(predicate, builder.greaterThanOrEqualTo(root.get("price"), minPrice));
        }
        ...
        Order priceOrder;
        CriteriaQuery<Product> criteriaQuery;
        priceOrder = builder.asc(root.get("price"));
        criteriaQuery = query
                        .where(predicate)
                        .orderBy(priceOrder);
        return entityManager
            .createQuery(criteriaQuery)
            .setFirstResult(pageNum * pageSize)
            .setMaxResults(pageSize)
            .getResultList();
    }
}

@Repository
public interface ProductRepository extends JpaRepository<Product, Long>, CustomProductRepository {
}

Test

依赖与配置

    testCompile('com.github.database-rider:rider-spring:1.2.9') {
        exclude group: 'org.slf4j', module: 'slf4j-simple'
    }
    testCompile('io.rest-assured:rest-assured:3.1.0')
    testCompile('org.springframework.boot:spring-boot-starter-test')

单元测试

单元测试为保证测试独立性,通常使用需要创建测试替身,下面仍然以Product为例:

ProductServiceTest

@RunWith(MockitoJUnitRunner.class)
public class ProductServiceTest {
    @Mock
    private ProductRepository productRepository;
    private ProductService productService;

    @Before
    public void setUp() throws Exception {
        productService = new ProductService(productRepository);
    }

    @Test
    public should_get_all_products() {
        //given
        Product product = ...;
        List<Product> products = new ArrayList<>();
        products.add(product);
        given(productRepository.findAll())
            .willReturn(products);
        //when
        List<Product> actual = productService.getAll();
        //then
        assertThat(...).isEqualTo(...);
    }

    @Test
    public void should_remove_a_product() {
        //given

        //when
        productService.remove(anyLong());
        //then
        verify(productRepository, times(1))
            .deleteById(anyLong());
    }
}

集成测试

使用Database Rider和RestAssured进行集成测试,为了保证测试独立性,需要设定测试数据库样本,默认在resouces/datasets目录下创建yml文件,示例如下:

{table_name}:
    - {id}: ...
      ...
    - {id}: ...
      ...

同时为了保证实际运行的数据库不被测试影响,需要新建一个application-test.yml配置文件,重建一个测试数据库,进行测试,然后在测试代码中加入@ActiveProfiles(“test”)指示测试使用的配置文件

ProductControllerTest

@SpringBootTest(webEnvironment = RANDOM_PROT)
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@DBRider
@DBUnit(caseSensitiveTableNames = true)
public class ProductControllerTest {
    @LocalServerTest
    prvate int port;

    @Test
    @DataSet("product.yml")
    //@ExpectedDataSet("expected_added_product.yml")
    public void should_add_a_product() {
        ...
        RestAssured
            .given()
            .port(port)
            .when()
            .contentType(ContentType.JSON)
            .body(newProduct)
            .post("/products")
            .then()
            .statusCode(201);
    }
}

Spring Cloud

依赖与配置

ext {
    springCloudVersion = 'Finchley.SR1'
}

...
    compile('org.springframework.cloud:spring-cloud-starter-security')
    compile('org.springframework.cloud:spring-cloud-starter-netflix-zuul')
    compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-server')


...

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

Security

控制服务的访问权限,以简单的通过数据库中的用户信息进行校验为例:

config/WebSecurityConfig

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @AutoWired
    private MyUserDetailService userDetailsService;

    @AutoWired
    public void globalConfig(AuthenticationManagerBuilder auth) throw Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(new BCryptPasswordEncoder());
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throw Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throw Exception {
        http
            .authorizeRequests()
            .anyMatchers(...).permitAll()//白名单
            .anyRequest().authenticated()
            .and()
            .httpBasic()
            .and()
            .csrf().disable();//解决post等403错误

    }
}

config/MyUserDetailService

@Component
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throw UsernameNotFoundException {
        User user = userRepository.findByUsername();

        if(user == null) {
            throw new UsernameNotFoundException({info});
        }
        else {
            return new JwtUser(user);
        }
    }
}

login/JwtUser

@Getter
@Setter
public class JwtUser implements UserDetails {
    private Long id;
    private String username;
    private String fullname;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser(User user) {
        ...
        this.authorities = Arrays.asList(new SimpleGrantedAuthority(user.getRole()));
        ...
    }
    public JwtUser() {}

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Zuul 智能路由

配置Zuul可以使用Zuul服务的端口访问多个微服务的接口:

在application.yml中,加入配置项,如下所示:

zuul:
    routes:
        products:
            path: /products/**
            url: http://localhost:xxxx/products
        ...
        app:
            path: /**
            url: http://localhost:xxxx

若要对接口进行部分业务处理,则可以加入filter,如下所示:

filter/mallFilter

@Component
public class MallFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }
    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        ...
        return null;
    }
}

Euraka

Euraka 负责服务注册与发现

服务注册

Euraka服务中添加@EnableEurekaServer注解,并在配置文件中添加以下内容即可:

spring:
  application:
    name: register-server
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

将服务在Eureka中注册只需要添加一个依赖一个注解一段配置

一个依赖:

    compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')

一个注解:@EnableEurekaClient

一段配置:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:{eureka_port}/eureka/

服务发现

若服务依赖另一个服务的接口,则需要该服务能够发现依赖服务,实现流程如下:

添加依赖:

compile('org.springframework.cloud:spring-cloud-starter-openfeign')

restService/client

@FeignClient("{dependence_server_name}")
public interface ProductClient {
    @GetMapping("/products/{id}")
    List<Product> get(@PathVariable(name="id" Long id));
    ...
}