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));
...
}